📝

だいたい100行で作る!Flutterの自動保存メモアプリ

2024/12/21に公開

この記事では、lifecycle_controller ライブラリを活用して、Flutterアプリのライフサイクルイベントとビジネスロジックをシームレスに組み合わせる方法を解説します。lifecycle_controllerは個人的に開発している状態管理ライブラリで、Providerパッケージをラップして作られています。

lifecycle_controllerはUIコードとビジネスロジックを分離するための仕組みを中心に、状態管理を行う際に便利な機能を備えています。
その機能を利用して、自動保存を行うことのできるメモアプリを100行程度のコードで実装してみたいと思います。

作成するアプリの機能

この記事で作成するアプリは、以下のような機能を持ちます:

  • リアルタイムでの自動保存:
    テキスト入力時にデータを自動的に保存します。
  • ライフサイクルイベントの対応:
    アプリが非アクティブになった際や戻る操作時にデータを保存します。
  • データの永続化:
    SharedPreferences を使用してデータを保持し、アプリを再起動してもメモ内容を復元できます。
  • デバウンスによる効率的な保存:
    短時間の連続入力でも無駄な保存処理を防ぎます。
  • イベント通知によるUI更新:
    保存完了時に SnackBar を使ってユーザーに通知します。

以下の内容を中心に進めます。

  1. LifecycleController の基本的な役割とメリット
  2. LifecycleController を活用した自動保存メモアプリの実装
  3. デバウンス機能による効率的な処理
  4. イベント通知機能を使ったUIとロジックの分離

1. LifecycleController の基本的な役割

LifecycleController は、Flutterウィジェットとビジネスロジックを分離するためのライブラリです。このライブラリを使うと、ウィジェットのライフサイクルイベント(初期化、非アクティブ状態、破棄など)を簡単に制御でき、ビジネスロジックとUIの密結合を防ぐことができます。

例えば、通常のFlutterアプリではライフサイクルイベントを State クラスや WidgetsBindingObserver を使って処理しますが、これではコードが複雑になりやすいです。一方で、LifecycleController を使うと以下のように簡潔に実装できます。

import 'package:lifecycle_controller/lifecycle_controller.dart';

class AutoSavePageController extends LifecycleController {
  
  void onInit() {
    super.onInit();
    print("onInit: 初期化処理");
  }

  
  void onInactive() {
    super.onInactive();
    print("onInactive: 非アクティブ状態になりました");
  }

  
  void onDispose() {
    super.onDispose();
    print("onDispose: リソースを解放します");
  }

  
  void onDidPop() {
    super.onDidPop();
    print("onDidPop: 戻る操作が行われました");
  }
}

これにより、ライフサイクルイベントに応じたロジックを整理して記述できます。また、onDidPop イベントを利用するためには、以下の設定を MaterialApp に追加する必要があります:

return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
    useMaterial3: true,
  ),
  navigatorObservers: [
    LifecycleController.basePageRouteObserver,
  ],
);

2. LifecycleController を使った自動保存メモの実装

次に、ユーザーが入力したテキストを自動で保存し、アプリを再起動しても内容を保持する機能を追加します。以下のコードでは、SharedPreferences を使ってデータを永続化します。

2-1. 初期化時にデータをロード

onInit メソッドで保存されたテキストをロードし、TextEditingController に設定します。

class AutoSavePageController extends LifecycleController {
  final String saveKey = 'auto_save_page_text';
  TextEditingController? textController;

  
  void onInit() async {
    super.onInit();
    textController = TextEditingController(
      text: (await _fetchText()) ?? '',
    );
    notifyListeners();
  }

  Future<String?> _fetchText() async {
    return (await SharedPreferences.getInstance()).getString(saveKey);
  }
}

2-2. 非アクティブ状態でデータを保存

アプリがバックグラウンドに入るとき、onInactive イベントで現在のテキストを保存します。

  
  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. スコープの定義とコントローラーの取得

LifecycleScope を使ってコントローラーのスコープを定義します。また、`LifecycleControllerChangeNotifierであり、内部でProviderパッケージを利用しているので、 context.readcontext.watchcontext.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. デバウンス機能による効率的な保存

LifecycleController には、デバウンス機能が備わっています。この機能を使うと、短時間に連続して呼び出された処理を抑制できます。以下のコードでは、onChanged イベントで入力されたテキストを1秒間待機してから保存します。

  void saveText(String text) async {
    debounce(
      id: 'save_text',
      duration: const Duration(seconds: 1),
      action: () async {
        await _saveText(text);
      },
    );
  }

4. イベント通知機能でUIとロジックを分離

LifecycleController では、イベント通知機能を使ってコントローラーからウィジェットに通知を送ることができます。これにより、保存が完了したときに SnackBar を表示するようなUI処理を、ウィジェット側で実装できます。

4-1. イベントの定義

保存イベント用のクラスを作成します。

abstract interface class AutoSavePageEvent {}

class AutoSavePageEventSaveText implements AutoSavePageEvent {
  final String text;

  AutoSavePageEventSaveText(this.text);
}

4-2. コントローラーからイベントを送信

保存完了時にイベントを発行します。

  Future<void> _saveText(String text) async {
    await (await SharedPreferences.getInstance()).setString(saveKey, text);
    emit(AutoSavePageEventSaveText(text));
  }

4-3. ウィジェットでイベントを処理

onEvent を使って、保存完了時に SnackBar を表示します。

  
  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,
            ),
          );
        }
      },
    );
  }

完全なコード例

この自動保存メモアプリの完全な実装は、LifecycleControllerの公式GitHubリポジトリ で確認できます。

まとめ

LifecycleController を使うことで、ライフサイクル管理、デバウンス処理、イベント通知を簡潔に実装できます。また、LifecycleScope を活用することでコントローラーのスコープを明確に定義し、Provider によるコントローラー管理で柔軟な状態管理が可能になります。

これにより、UIとビジネスロジックを分離し、よりクリーンなコードを書くことが可能です。ぜひ試してみてください!

Discussion