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 SharedPreferencesto 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 (1)
Best article, every, I was looking into this and the docs suck...thank you brother.