This article demonstrates how to use the Lifecycle Controller library to seamlessly combine Flutter app lifecycle events with business logic. By leveraging this approach, you can decouple UI code from business logic, resulting in a highly maintainable app. Moreover, this app can be implemented in around 100 lines of code, keeping it compact and efficient.
Features of the App
The app we will build includes the following features:
- Real-time Auto-Saving: Automatically saves data as the user enters text.
- Lifecycle Event Handling: Saves data when the app becomes inactive or when the user navigates back.
-
Persistent Data:
Uses
SharedPreferences
to store data, ensuring memo contents persist even after restarting the app. - Debounced Saving: Prevents redundant save operations during rapid text input.
-
UI Updates via Event Notifications:
Notifies the user of save completion using a
SnackBar
.
This guide will focus on:
- The basic role and benefits of
LifecycleController
- Implementing the auto-saving memo app with
LifecycleController
- Efficient processing with the debounce feature
- Separating UI and logic using event notification functionality
1. The Role of LifecycleController
LifecycleController
is a library designed to decouple Flutter widgets from business logic. It simplifies managing widget lifecycle events such as initialization, inactivity, and disposal while avoiding tight coupling between UI and business logic.
In a typical Flutter app, lifecycle events are handled using the State
class or WidgetsBindingObserver
, which can make the code more complex. In contrast, LifecycleController
allows for a concise implementation, as shown below:
import 'package:lifecycle_controller/lifecycle_controller.dart';
class AutoSavePageController extends LifecycleController {
@override
void onInit() {
super.onInit();
print("onInit: Initialization process");
}
@override
void onInactive() {
super.onInactive();
print("onInactive: The app is now inactive");
}
@override
void onDispose() {
super.onDispose();
print("onDispose: Resources are being released");
}
@override
void onDidPop() {
super.onDidPop();
print("onDidPop: Back navigation was triggered");
}
}
This implementation organizes lifecycle event logic clearly. To enable the onDidPop
event, the following configuration must be added to the MaterialApp
:
return MaterialApp(
title: "'Flutter Demo',"
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
navigatorObservers: [
LifecycleController.basePageRouteObserver,
],
);
2. Implementing Auto-Saving Memo with LifecycleController
Next, we will add functionality to save user-entered text automatically and persist it across app restarts. The following code uses SharedPreferences
for data persistence.
2-1. Loading Data on Initialization
The onInit
method loads previously saved text and assigns it to a TextEditingController
.
class AutoSavePageController extends LifecycleController {
final String saveKey = 'auto_save_page_text';
TextEditingController? textController;
@override
void onInit() async {
super.onInit();
textController = TextEditingController(
text: (await _fetchText()) ?? '',
);
notifyListeners();
}
Future<String?> _fetchText() async {
return (await SharedPreferences.getInstance()).getString(saveKey);
}
}
2-2. Saving Data When Inactive
The onInactive
event saves the current text when the app enters the background.
@override
void onInactive() {
super.onInactive();
_saveText(textController?.text ?? '');
}
Future<void> _saveText(String text) async {
await (await SharedPreferences.getInstance()).setString(saveKey, text);
print("Text saved: $text");
}
2-3. Defining Scope and Retrieving Controller
The LifecycleScope
is used to define the controller's scope. Using Provider
, the controller can be accessed with context.read
, context.watch
, and context.select
.
return LifecycleScope.create(
create: () => AutoSavePageController(),
builder: (context) {
final textController =
context.select<AutoSavePageController, TextEditingController?>((controller) => controller.textController);
return Scaffold(
appBar: AppBar(
title: const Text('Auto Save Memo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: textController,
minLines: 1,
maxLines: 30,
onChanged: (text) {
context.read<AutoSavePageController>().saveText(text);
},
decoration: const InputDecoration.collapsed(
hintText: 'Enter text',
),
),
),
);
},
);
3. Efficient Saving with Debounce
LifecycleController
includes a debounce feature to suppress redundant calls within a short time. The following code debounces save operations when text input changes.
void saveText(String text) async {
debounce(
id: 'save_text',
duration: const Duration(seconds: 1),
action: () async {
await _saveText(text);
},
);
}
4. Separating UI and Logic with Event Notifications
LifecycleController
allows event notifications to send messages from the controller to the widget, enabling UI updates like showing a SnackBar
on save completion.
4-1. Defining Events
Create a class for save events.
abstract interface class AutoSavePageEvent {}
class AutoSavePageEventSaveText implements AutoSavePageEvent {
final String text;
AutoSavePageEventSaveText(this.text);
}
4-2. Emitting Events from the Controller
The controller emits an event after saving text.
Future<void> _saveText(String text) async {
await (await SharedPreferences.getInstance()).setString(saveKey, text);
emit(AutoSavePageEventSaveText(text));
}
4-3. Handling Events in the Widget
Use onEvent
to handle save completion events and display a SnackBar
.
@override
Widget build(BuildContext context) {
return LifecycleScope.create(
create: () => AutoSavePageController(),
builder: (context) {
final textController =
context.select<AutoSavePageController, TextEditingController?>((controller) => controller.textController);
return Scaffold(
appBar: AppBar(
title: const Text('Auto Save Memo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: textController,
minLines: 1,
maxLines: 30,
onChanged: (text) {
context.read<AutoSavePageController>().saveText(text);
},
decoration: const InputDecoration.collapsed(
hintText: 'Enter text',
),
),
),
);
},
onEvent: (context, controller, event) {
if (event is AutoSavePageEventSaveText) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Successfully saved',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
backgroundColor: Colors.green,
),
);
}
},
);
}
Full Code Example
You can find the complete implementation of this auto-saving memo app in the official LifecycleController GitHub repository.
Conclusion
Using LifecycleController
, you can easily manage lifecycle events, debounce operations, and handle UI updates via event notifications. With LifecycleScope
, controller scope can be clearly defined, and Provider
enables flexible state management.
This approach simplifies code by separating UI from business logic, making it cleaner and more maintainable. Give it a try in your next project!
Top comments (0)