This guide will help you migrate from Control 0.x to 1.0.0.
Version 1.0.0 introduces significant architectural improvements:
- Removed
basemodifiers - More flexibility in inheritance - Default concurrent behavior - No forced mixin selection
- Simplified mixins - Built on top of Mutex
- New
handle()signature - Addederroranddonecallbacks
Before (0.x):
final class MyController extends StateController<MyState>
with SequentialControllerHandler {
// ...
}After (1.0.0):
class MyController extends StateController<MyState>
with SequentialControllerHandler {
// ...
}Why: The base modifier forced users to use final or base on their controller classes. Removing it provides more flexibility.
Action Required: Remove base or final modifiers from your controller class declarations if you want more flexibility. You can keep final if you prefer.
Before (0.x):
class MyController extends StateController<MyState>
with ConcurrentControllerHandler {
void operation() => handle(() async { ... });
}After (1.0.0):
class MyController extends StateController<MyState> {
// Concurrent by default - no mixin needed!
void operation() => handle(() async { ... });
}Why: The base Controller class now provides concurrent behavior by default. The mixin is redundant.
Action Required: Remove with ConcurrentControllerHandler from your controller declarations. The mixin is deprecated but still available for backwards compatibility.
The handle() method is now generic and can return values:
Before (0.x):
void fetchData() => handle(() async {
final data = await api.fetch();
setState(state.copyWith(data: data));
// No return value
});After (1.0.0):
// Still works without return value
void fetchData() => handle(() async {
final data = await api.fetch();
setState(state.copyWith(data: data));
});
// NEW: Can now return values
Future<User> fetchUser(String id) => handle<User>(() async {
final user = await api.getUser(id);
setState(state.copyWith(user: user));
return user; // Type-safe return value!
});Action Required: None for existing code. This is backward compatible.
Before (0.x):
// Dropped operations completed with Future<void>.value()
await controller.operation(); // voidAfter (1.0.0):
// Dropped operations return null with Future<T?>.value(null)
final result = await controller.operation(); // null if dropped
if (result != null) {
// Operation completed successfully
print('Result: $result');
} else {
// Operation was dropped
print('Operation dropped: controller is busy');
}Action Required: None for existing code. The behavior is backward compatible - dropped operations return null (expected behavior). If you need to distinguish between dropped operations and successful operations, check for null return values.
The handle() method signature has been extended:
Before (0.x):
void operation() => handle(
() async { ... },
name: 'operation',
meta: {'key': 'value'},
);After (1.0.0):
void operation() => handle(
() async { ... },
error: (error, stackTrace) async {
// Optional: handle errors
},
done: () async {
// Optional: cleanup or completion logic
},
name: 'operation',
meta: {'key': 'value'},
);Why: The new parameters were always available in mixins but not in the base interface. Now they're standardized.
Action Required:
- No action required -
erroranddoneare optional - If you were using custom error handling in mixins, you can now use it in all controllers
- Update your code if you want to use the new error handling capabilities
Before:
final class MyController extends StateController<MyState>
with ConcurrentControllerHandler {
MyController() : super(initialState: MyState.initial());
void fetchData() => handle(() async {
final data = await api.fetch();
setState(state.copyWith(data: data));
});
}After:
class MyController extends StateController<MyState> {
MyController() : super(initialState: MyState.initial());
void fetchData() => handle(() async {
final data = await api.fetch();
setState(state.copyWith(data: data));
});
}Changes:
- Removed
finalmodifier - Removed
with ConcurrentControllerHandler
Before:
final class MyController extends StateController<MyState>
with SequentialControllerHandler {
void operation1() => handle(() async { ... });
void operation2() => handle(() async { ... });
}After:
class MyController extends StateController<MyState>
with SequentialControllerHandler {
void operation1() => handle(() async { ... });
void operation2() => handle(() async { ... });
}Changes:
- Only removed
finalmodifier (optional) - Mixin works the same way
Before:
final class MyController extends StateController<MyState>
with DroppableControllerHandler {
void operation() => handle(() async { ... });
}After:
class MyController extends StateController<MyState>
with DroppableControllerHandler {
void operation() => handle(() async { ... });
}Changes:
- Only removed
finalmodifier (optional) - Mixin works the same way
New in 1.0.0: You can now use Mutex directly for custom patterns:
class MyController extends StateController<MyState> {
MyController() : super(initialState: MyState.initial());
final _criticalMutex = Mutex();
final _batchMutex = Mutex();
// Sequential critical operations
void saveToDB() => _criticalMutex.synchronize(
() => handle(() async {
// Critical database operation
}),
);
// Sequential batch operations
void processBatch() => _batchMutex.synchronize(
() => handle(() async {
// Batch processing
}),
);
// Concurrent queries (default behavior)
void fetchData() => handle(() async {
// Fast concurrent query
});
// Droppable UI actions
void onButtonTap() {
final unlock = _criticalMutex.tryLock();
if (unlock == null) return; // Already running, drop
handle(() async {
// Handle button tap
}).whenComplete(unlock);
}
}-
Update dependency in pubspec.yaml:
dependencies: control: ^1.0.0
-
Run Flutter pub get:
flutter pub get
-
Find all controllers:
grep -r "extends.*Controller" lib/ -
For each controller:
- Remove
baseorfinalmodifier (optional but recommended) - Remove
with ConcurrentControllerHandlerif present - Keep
with SequentialControllerHandlerorwith DroppableControllerHandler - Consider using Mutex directly for complex scenarios
- Remove
-
Run tests:
flutter test -
Check for deprecation warnings:
flutter analyze
Solution: This is just an informational message. The code works correctly. The IDE may show this while it updates its cache.
Solution: Remove with ConcurrentControllerHandler from your controller. Concurrent is now the default behavior.
Solution:
- Ensure all test controllers are updated
- Check if tests rely on specific timing - concurrent behavior may differ
- Update mocks if using custom mixins
After migrating to 1.0.0, you'll benefit from:
- More flexibility - No forced
finalmodifier - Cleaner code - No redundant
ConcurrentControllerHandler - Better control - Use Mutex directly for custom patterns
- Simpler architecture - Less boilerplate, easier to understand
- Same guarantees - All existing tests pass
If you encounter issues during migration:
- Check the examples directory for updated patterns
- Review IDEAS.md for architecture explanation
- Open an issue on GitHub
If you need to rollback to 0.x:
dependencies:
control: ^0.2.0Then restore your base/final modifiers and ConcurrentControllerHandler mixins.