DEV Community

Cover image for Build an Auto-Saving Memo App in 100 Lines with Flutter
skyaa
skyaa

Posted on

Build an Auto-Saving Memo App in 100 Lines with Flutter

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:

  1. The basic role and benefits of LifecycleController
  2. Implementing the auto-saving memo app with LifecycleController
  3. Efficient processing with the debounce feature
  4. 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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  ],
);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
Enter fullscreen mode Exit fullscreen mode

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',
          ),
        ),
      ),
    );
  },
);
Enter fullscreen mode Exit fullscreen mode

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);
      },
    );
  }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
  }
Enter fullscreen mode Exit fullscreen mode

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,
            ),
          );
        }
      },
    );
  }
Enter fullscreen mode Exit fullscreen mode

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)