Lifecycle Controller is a Flutter library designed to eliminate the boilerplate associated with the ChangeNotifier pattern from the Provider package. By leveraging Provider as the underlying library, LifecycleWidget simplifies state management and lifecycle handling in your applications. It provides a structured and intuitive approach to managing screen lifecycle events, local state, and UI updates, enabling you to build robust, scalable, and maintainable Flutter applications with ease.
Managing state and lifecycle events in Flutter can involve repetitive boilerplate code, especially when using the ChangeNotifier pattern with Provider. As your application grows, maintaining a clean separation between UI and business logic becomes crucial. Lifecycle Controller addresses these challenges by:
- Reducing Boilerplate: Eliminates repetitive code associated with the ChangeNotifier pattern, allowing you to focus on your app's logic.
- Simplifying State Management: Provides a clean, provider-based architecture for managing local state without unnecessary overhead.
- Handling Lifecycle Events: Offers built-in methods to handle screen lifecycle events like navigation changes and app lifecycle states.
- Enhancing Maintainability: Promotes separation of concerns, making your codebase cleaner and easier to maintain.
- Customizable UI Components: Allows easy customization of loading and error screens to match your app's design.
- Efficient Subscription Management: Simplifies stream subscription handling to prevent memory leaks.
- Debouncing and Throttling: Provides methods to debounce and throttle actions to optimize performance.
What sets Lifecycle Controller apart from other state management libraries is that it provides essential features for separating Widgets from business logic, such as loading state management, efficient subscription handling, and convenient functions like debouncing and throttling. These built-in features simplify complex state management and performance optimization, making it easier to implement.
Add Lifecycle Controller to your pubspec.yaml
file:
dependencies:
lifecycle_controller: latest_version
provider: latest_version
Then run:
flutter pub get
Create a controller by extending LifecycleController
. This controller will manage the state and logic for your screen.
class CounterController extends LifecycleController {
int _counter = 0;
int get counter => _counter;
void increment() {
// Use `asyncRun` to handle loading states and errors automatically
asyncRun(() async {
await Future.delayed(const Duration(seconds: 1));
_counter++;
notifyListeners(); // Notify listeners to rebuild UI
});
}
@override
void onInit() {
super.onInit();
// Initialization logic here
print('CounterController initialized');
}
@override
void onDispose() {
super.onDispose();
// Cleanup logic here
print('CounterController disposed');
}
}
Using LifecycleScope
, you can create a controller. Within this LifecycleScope
, you are free to use custom-created controllers by utilizing the approach provided by the Provider package.
class CounterWidget extends StatelessWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
return LifecycleScope(
builder: (context) {
final controller = context.read<CounterController>();
final counter = context.select<CounterController, int>(
(value) => value.counter,
);
return Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: Text(
'Count: $counter',
),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
},
controller: CounterController(),
);
}
}
Lifecycle Controller leverages the power of the Provider package for state management. It automatically sets up the necessary Provider infrastructure, allowing you to access your controller's state easily within your screen.
Access your controller and its state using context.read
, context.watch
, or context.select
:
// Read the controller instance (does not listen for changes)
final controller = context.read<CounterController>();
// Watch for all changes in the controller (rebuilds on any change)
final counter = context.watch<CounterController>().counter;
// Select and listen to specific properties (optimal for performance)
final counter = context.select<CounterController, int>(
(controller) => controller.counter,
);
π‘ Tip: Use context.select
when you want to listen to specific properties to optimize performance and prevent unnecessary rebuilds.
Lifecycle Controller provides several lifecycle hooks that you can override in your controller to respond to various lifecycle events:
class MyController extends LifecycleController {
@override
void onInit() {
super.onInit();
// Called when the controller is initialized
}
@override
void onDispose() {
super.onDispose();
// Called when the controller is disposed
}
@override
void onDidPush() {
super.onDidPush();
// Called when the screen is pushed onto the navigation stack
}
@override
void onDidPop() {
super.onDidPop();
// Called when the screen is popped from the navigation stack
}
@override
void onDidPushNext() {
super.onDidPushNext();
// Called when a new screen is pushed on top of this one
}
@override
void onDidPopNext() {
super.onDidPopNext();
// Called when the screen on top of this one is popped
}
@override
void onResumed() {
super.onResumed();
// Called when the app is resumed from the background
}
@override
void onInactive() {
super.onInactive();
// Called when the app becomes inactive
}
@override
void onPaused() {
super.onPaused();
// Called when the app is paused
}
@override
void onDetached() {
super.onDetached();
// Called when the app is detached
}
}
If you want to use methods related to routing, such as onDidPop
, you must set up a RouteObserver
.
Widget build(BuildContext context) {
...
return new MaterialApp(
...
navigatorObservers: <NavigatorObserver>[
LifecycleController.basePageRouteObserver,
],
...
);
}
Manage asynchronous tasks with ease using the asyncRun
method. It automatically handles loading states and error management.
class DataController extends LifecycleController {
List<String> _items = [];
List<String> get items => _items;
void fetchData() {
asyncRun(() async {
// Simulate network request
await Future.delayed(const Duration(seconds: 2));
_items = ['Item 1', 'Item 2', 'Item 3'];
notifyListeners(); // Update UI
});
}
}
In your widget, you can show a loading indicator or error message based on the controller's state.
@override
Widget build(BuildContext context, DataController controller) {
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
} else if (controller.isError) {
return Center(child: Text('Error: ${controller.errorMessage}'));
} else {
return ListView(
children: controller.items.map((item) => ListTile(title: Text(item))).toList(),
);
}
}
Override the buildLoading
method in your widget to provide a custom loading UI.
class MyWidget extends LifecycleWidget<MyController> {
@override
Widget buildLoading(BuildContext context, MyController controller) {
return const Center(
child: CircularProgressIndicator(color: Colors.red),
);
}
}
Override the buildError
method to customize the error UI.
class MyWidget extends LifecycleWidget<MyController> {
@override
Widget buildError(BuildContext context, MyController controller) {
return Center(
child: Text(
'Oops! ${controller.errorMessage}',
style: TextStyle(color: Colors.red, fontSize: 18),
),
);
}
}
Manage stream subscriptions efficiently using built-in methods to prevent memory leaks.
Use the addSubscription
method to add a subscription that the controller will manage.
class MyController extends LifecycleController {
void listenToStream(Stream<int> stream) {
final subscription = stream.listen((data) {
// Handle incoming data
});
addSubscription(subscription);
}
}
All added subscriptions are automatically cancelled when the controller is disposed. You can also manually cancel subscriptions if needed.
// Cancel a specific subscription
await cancelSubscription(subscription);
// Cancel all subscriptions
await cancelSubscriptionAll();
Use the debounce
method to prevent a function from being called too frequently.
class SearchController extends LifecycleController {
void onSearchChanged(String query) {
debounce(const Duration(milliseconds: 500), () {
// Perform search
}, id: 'search');
}
}
Use the throttle
method to limit the rate at which a function can be called.
class ScrollController extends LifecycleController {
void onScroll(double offset) {
throttle(const Duration(milliseconds: 200), () {
// Handle scroll offset
}, id: 'scroll');
}
}
-
Keep Controllers Focused: Each controller should manage state for a single screen or feature to maintain clarity and ease of testing.
-
Use
asyncRun
for Async Tasks: UtilizeasyncRun
to handle loading and error states automatically, ensuring consistent UX. -
Leverage Lifecycle Hooks: Override lifecycle methods to initialize resources, dispose of them, and respond to navigation events appropriately.
-
Optimize UI Rebuilds: Use
context.select
to listen to specific properties, reducing unnecessary widget rebuilds and improving performance. -
Handle Errors Gracefully: Always provide user feedback by handling errors using
showError
and customizing the error UI.
A: Lifecycle Controllert focuses on providing a simple, provider-based approach to state management with a strong emphasis on lifecycle events. It aims to reduce boilerplate and integrate seamlessly with Flutter's navigation and lifecycle.
A: No, the controller is automatically disposed of when the widget is removed from the widget tree.
Contributions are welcome! If you have suggestions for improvements, new features, or find any issues, please open an issue or submit a pull request on GitHub.
This project is licensed under the MIT License.