control
Flutter के लिए सरल राज्य प्रबंधन, एकसाथ कार्यान्वयन का समर्थन करता है।
Flutter के लिए सरल राज्य प्रबंधन, एकसाथ कार्यान्वयन का समर्थन करता है।
यह अंग्रेज़ी मूल स्नैपशॉट है। नवीनतम सामग्री GitHub पर देखें।
A simple, flexible state management library for Flutter with built-in concurrency support.
Add the following dependency to your pubspec.yaml file:
dependencies:
control: ^1.0.0
/// Counter state
typedef CounterState = ({int count, bool idle});
/// Counter controller - concurrent by default
class CounterController extends StateController<CounterState> {
CounterController({CounterState? initialState})
: super(initialState: initialState ?? (idle: true, count: 0));
void increment() => handle(() async {
setState((idle: false, count: state.count));
await Future<void>.delayed(const Duration(milliseconds: 500));
setState((idle: true, count: state.count + 1));
});
void decrement() => handle(() async {
setState((idle: false, count: state.count));
await Future<void>.delayed(const Duration(milliseconds: 500));
setState((idle: true, count: state.count - 1));
});
}
Operations execute in parallel without waiting for each other:
class MyController extends StateController<MyState> {
MyController() : super(initialState: MyState.initial());
// These operations run concurrently
void operation1() => handle(() async { ... });
void operation2() => handle(() async { ... });
}
Operations execute one after another in FIFO order:
class MyController extends StateController<MyState>
with SequentialControllerHandler {
MyController() : super(initialState: MyState.initial());
// These operations run sequentially
void operation1() => handle(() async { ... });
void operation2() => handle(() async { ... });
}
New operations are dropped if one is already running:
class MyController extends StateController<MyState>
with DroppableControllerHandler {
MyController() : super(initialState: MyState.initial());
// If operation1 is running, operation2 is dropped
void operation1() => handle(() async { ... });
void operation2() => handle(() async { ... });
}
Use Mutex directly for fine-grained control:
class MyController extends StateController<MyState> {
MyController() : super(initialState: MyState.initial());
final _criticalMutex = Mutex();
final _batchMutex = Mutex();
// Sequential critical operations
void criticalOperation() => _criticalMutex.synchronize(
() => handle(() async { ... }),
);
// Sequential batch operations (different queue)
void batchOperation() => _batchMutex.synchronize(
() => handle(() async { ... }),
);
// Concurrent fast operations
void fastOperation() => handle(() async { ... });
}
The handle() method is generic and can return values:
class UserController extends StateController<UserState> {
UserController(this.api) : super(initialState: UserState.initial());
final UserApi api;
/// Fetch user and return the user object
Future<User> fetchUser(String id) => handle<User>(() async {
final user = await api.getUser(id);
setState(state.copyWith(user: user, loading: false));
return user; // Type-safe return value
});
/// Update user and return success status
Future<bool> updateUser(User user) => handle<bool>(() async {
try {
await api.updateUser(user);
setState(state.copyWith(user: user));
return true;
} catch (e) {
return false;
}
});
}
// Usage
final user = await controller.fetchUser('123');
print('Fetched: ${user.name}');
final success = await controller.updateUser(updatedUser);
if (success) {
print('User updated successfully');
}
Note: With DroppableControllerHandler, dropped operations return null instead of executing.
Use ControllerScope to provide controller to widget tree:
class App extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
home: ControllerScope<CounterController>(
CounterController.new,
child: const CounterScreen(),
),
);
}
Use StateConsumer to rebuild widgets when state changes:
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
body: StateConsumer<CounterController, CounterState>(
builder: (context, state, _) => Text('Count: ${state.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.controllerOf<CounterController>().increment(),
child: Icon(Icons.add),
),
);
}
Convert state to ValueListenable for granular updates:
ValueListenableBuilder<bool>(
valueListenable: controller.select((state) => state.idle),
builder: (context, isIdle, _) => ElevatedButton(
onPressed: isIdle ? () => controller.increment() : null,
child: Text('Increment'),
),
)
The handle() method provides built-in error handling:
void riskyOperation() => handle(
() async {
// Your operation
throw Exception('Something went wrong');
},
error: (error, stackTrace) async {
// Handle error
print('Error: $error');
},
done: () async {
// Always called, even if error occurs
print('Operation completed');
},
name: 'riskyOperation', // For debugging
);
Monitor all controller events for debugging:
class MyObserver implements IControllerObserver {
@override
void onCreate(Controller controller) {
print('Controller created: ${controller.name}');
}
@override
void onHandler(HandlerContext context) {
print('Handler started: ${context.name}');
}
@override
void onStateChanged<S extends Object>(
StateController<S> controller,
S prevState,
S nextState,
) {
print('State changed: $prevState -> $nextState');
}
@override
void onError(Controller controller, Object error, StackTrace stackTrace) {
print('Error in ${controller.name}: $error');
}
@override
void onDispose(Controller controller) {
print('Controller disposed: ${controller.name}');
}
}
void main() {
Controller.observer = MyObserver();
runApp(MyApp());
}
Use Mutex for custom synchronization:
final mutex = Mutex();
// Method 1: synchronize (automatic unlock)
await mutex.synchronize(() async {
// Critical section
});
// Method 2: lock/unlock (manual control)
final unlock = await mutex.lock();
try {
// Critical section
if (someCondition) {
unlock();
return; // Early exit
}
// More code
} finally {
unlock();
}
// Check if locked
if (mutex.locked) {
print('Mutex is currently locked');
}
See MIGRATION.md for detailed migration guide.
Key changes:
base from controller classesConcurrentControllerHandler is deprecated (remove it)Mutex for custom concurrency patternsChoose the right concurrency strategy:
Use handle() for all async operations:
Keep state immutable:
Dispose controllers:
ControllerScopeUse error and done callbacks to provide user feedback through SnackBars, dialogs, or notifications:
class UserController extends StateController<UserState> {
UserController(this.api) : super(initialState: UserState.initial());
final UserApi api;
Future<User?> updateProfile(
User user, {
void Function(User user)? onSuccess,
void Function(Object error)? onError,
}) => handle<User>(
() async {
final updatedUser = await api.updateUser(user);
setState(state.copyWith(user: updatedUser));
onSuccess?.call(updatedUser);
return updatedUser;
},
error: (error, stackTrace) async {
onError?.call(error);
},
name: 'updateProfile',
meta: {'userId': user.id},
);
}
// Usage in UI
ElevatedButton(
onPressed: () => controller.updateProfile(
updatedUser,
onSuccess: (user) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Profile updated: ${user.name}')),
);
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $error'),
backgroundColor: Colors.red,
),
);
},
),
child: const Text('Update Profile'),
)
Add interactive dialogs in the middle of processing for user input:
class AuthController extends StateController<AuthState> {
AuthController(this.api) : super(initialState: AuthState.initial());
final AuthApi api;
Future<bool?> login(
String email,
String password, {
required Future<String> Function() requestSmsCode,
}) => handle<bool>(
() async {
// Step 1: Initial login
final session = await api.login(email, password);
// Step 2: Check if 2FA is required
if (session.requires2FA) {
// Request SMS code from user via dialog
final smsCode = await requestSmsCode();
// Step 3: Verify SMS code
await api.verify2FA(session.id, smsCode);
}
setState(state.copyWith(isAuthenticated: true));
return true;
},
error: (error, stackTrace) async {
setState(state.copyWith(error: error.toString()));
},
name: 'login',
meta: {'email': email, 'requires2FA': true},
);
}
// Usage in UI
ElevatedButton(
onPressed: () => controller.login(
email,
password,
requestSmsCode: () async {
// Show dialog and wait for user input
final code = await showDialog<String>(
context: context,
builder: (context) => SmsCodeDialog(),
);
return code ?? '';
},
),
child: const Text('Login'),
)
Use name and meta parameters for debugging, logging, and integration with error tracking services like Sentry or Crashlytics:
class ControllerObserver implements IControllerObserver {
const ControllerObserver();
@override
void onHandler(HandlerContext context) {
// Log operation start with metadata
print('START | ${context.controller.name}.${context.name}');
print('META | ${context.meta}');
final stopwatch = Stopwatch()..start();
context.done.whenComplete(() {
// Log operation completion with duration
stopwatch.stop();
print('DONE | ${context.controller.name}.${context.name} | '
'duration: ${stopwatch.elapsed}');
});
}
@override
void onError(Controller controller, Object error, StackTrace stackTrace) {
final context = Controller.context;
if (context != null) {
// Send breadcrumbs to Sentry/Crashlytics
Sentry.addBreadcrumb(Breadcrumb(
message: '${controller.name}.${context.name}',
data: context.meta,
level: SentryLevel.error,
));
// Report error with full context
Sentry.captureException(
error,
stackTrace: stackTrace,
hint: Hint.withMap({
'controller': controller.name,
'operation': context.name,
'metadata': context.meta,
}),
);
}
}
@override
void onStateChanged<S extends Object>(
StateController<S> controller,
S prevState,
S nextState,
) {
final context = Controller.context;
// Log state changes with operation context
if (context != null) {
print('STATE | ${controller.name}.${context.name} | '
'$prevState -> $nextState');
print('META | ${context.meta}');
}
}
@override
void onCreate(Controller controller) {
print('CREATE | ${controller.name}');
}
@override
void onDispose(Controller controller) {
print('DISPOSE | ${controller.name}');
}
}
// Setup in main
void main() {
Controller.observer = const ControllerObserver();
runApp(const App());
}
Benefits of using name and meta:
See example/ directory for complete examples:
Refer to the Changelog to get all release notes.
If you want to support the development of our library, there are several ways you can do it:
We appreciate any form of support, whether it's a financial donation or just a star on GitHub. It helps us to continue developing and improving our library. Thank you for your support!
निर्भरता सूची 4 आइटम
{"sdk":"flutter"}^1.17.0{"sdk":"flutter"}^6.0.0उपयोग के अनुसार छाँटें, गतिविधि की तुलना करें और अपनाने से पहले मूल प्रोजेक्ट की आवश्यकताएँ जाँचें।
“A repository is the most basic element of GitHub.”
GitHub Docs — About repositories ↗