NotifierProviderを用いた同期的なUI更新

こんにちは、株式会社Pentagonでアプリ開発をしている中原です。

RiverpodのNotifierProviderを使用してUIを更新する方法について学習したため、記事にまとめました。

NotifierProviderはRiverpod2.0からProviderに追加されました。以前からあるStateNotiferProviderの使用は非推奨となり、NotifierProvider、AsyncNotifierProviderの使用が推奨となりました。今回はNotifierProviderの基本的な使い方についてご紹介します。

【こんな人に読んで欲しい】

  • Flutterアプリ開発エンジニア
  • Riverpod 2.0以降、情報をアップデートしていない人

【この記事を読むメリット】

  • 同期的に状態を監視して、UIを更新することができる。
  • NotifierProviderの基本的な使い方を理解できる。

【結論】
AsyncNotifierProviderは非同期で状態を更新を行いたい場合、NotifierProviderは同期で状態を更新を行いたい場合に使用します。
NotifierProviderを用いた同期的なUIの更新を行うことで、処理の流れを明確に把握することができ保守性を高めることができます。
どちらを使用するかは、処理の内容に合わせて選択する必要があります。
それでは、NotifierProviderの使い方を見ていきましょう。

目次

この記事で触れないこと

この記事では、Todoアプリの実装においてNotifierProviderを使用しています。以下の実装や説明については省略しています。

  • Flutterの開発環境構築
  • Todoモデルの実装
  • Freezedの説明

開発環境

  • Flutter 3.10.1
  • Dart 3.0.1
  • vscode

パッケージ

NotifierProviderはパッケージにflutter_riverpodを追加することで使用することができます。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.3.6
  freezed_annotation: ^2.2.0
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  freezed: ^2.3.5
  build_runner: ^2.4.5
  json_serializable: ^6.7.0

NotifierProviderとは

NotifierProviderは、同期の状態とそれを操作するNotifierを提供するためのProviderです。Notifierの状態が変更されるたびに通知され、状態の変更に応じて同期的に値を更新することができます。通常、次の場合に使用されます。

  • イベント処理後に時間の経過とともに状態(state)が変化する場合
  • ビジネスロジックを一箇所に集中させ、長期的な保守性を向上させたい場合

NotifierProviderの実装

NotifierProviderを使用するためには、Notifierを継承したクラスを作成する必要があります。
Notifierを使用する場合、初めにbuildメソッドを作成する必要があり、NotifierProviderを呼び出した際に最初に呼ばれるメソッドです。buildメソッド内には初期値を返す処理を書く必要があります。

NotifierProviderは次の方法で定義して使用します。

final プロバイダ名 = NotifierProvider<T>((ref) => Class名.new);

todoNotifierProviderの実装

import 'dart:math';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_notifier_1/model/todo.dart';

// NotifierProviderを定義。
final todoNotifierProvider =
    NotifierProvider<TodoNotifier, List<Todo>>(TodoNotifier.new);

// Notifierの定義
class TodoNotifier extends Notifier<List<Todo>> {
  // TodoNotifierの初期化
  // build()メソッドが初めに呼ばれる
  @override
  List<Todo> build() {
    return [
      Todo(id: '1', title: '昼寝する'),
    ];
  }

  // Todoの追加
  void add(String title) {
    // 一つの処理が終わるまで、次の処理は実行されない。

    print("todo追加始まり"); //1番目に処理が実行される。

    for (int i = 0; i < 3; i++) { //2番目に処理が実行される。
      final todo = Todo(id: Random().nextDouble().toString(), title: title);
      print("${i}番目のtodo追加");  //3番目に処理が実行される。

      // スプレッド演算子を使って、stateのリストに新しいTodoを追加する
      // [Todo(id: '1'),(新しいTodo)]
      state = [...state, todo];
    }

    print("todo追加終わり"); // 4番目 2番目の処理が全て実行されてから実行される。
  }

  // Todoの削除
  void remove(Todo todo) {
    // stateのリストから、引数のTodoを除外する。
    state = state.where((item) => item.id != todo.id).toList();
  }

  // ✅ Todoの完了・未完了の切り替え
  void toggle(Todo todo) {
    // stateのリストから、引数のTodoのcompletedを反転させる。
    state = state.map((item) {
      if (item.id == todo.id) return item.copyWith(completed: !item.completed);
      return item;
    }).toList();
  }
}

上記のコードでは、NotifierProviderを定義し、NotifierのClassにはデータの追加、更新、削除などの機能を定義しています。必要に応じてプロパティやメソッドを追加してください。

NotifierProviderを使用してUIを更新する

NotifierProviderは、ConsumerWidgetやHookWidgetなど、WidgetRefを持つウィジェットで使用します。NotifierProviderは ref.watch(プロバイダー名); のように呼び出され、状態(state)を取得することができます。また、ref.read(プロバイダー名.notifier); とすることでNotifierのメソッドを呼び出すことができます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'notifier_provider.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'sample',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // NotifierProviderを呼び出す。
    final todoList = ref.watch(todoNotifierProvider);

    // .notifierを追加して、Notifierのメソッドを呼び出す。
    // メソッドの呼び出しにはref.read()を使用する。
    final todoNotifier = ref.read(todoNotifierProvider.notifier);

    final TextEditingController controller = TextEditingController();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("sample"),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30),
        child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
          // テキストフィールドとTodo作成ボタン
          Row(
            children: [
              Expanded(
                child: TextFormField(
                  controller: controller,
                  maxLength: 20,
                ),
              ),
              IconButton(
                onPressed: () {
                  // Todoの追加
                  // 1つずつ順番に処理を実行するので、前の処理が終わるまで次の処理は実行されない。
                  todoNotifier.add(controller.text);
                },
                icon: const Icon(Icons.play_arrow),
              )
            ],
          ),
          const Padding(
            padding: EdgeInsets.symmetric(vertical: 30),
            child: Text("TODO"),
          ),
          // 初期値にTodoデータを持っているためnull判定は記述しない。
          Expanded(
            child: ListView.builder(
              itemCount: todoList.length,
              itemBuilder: ((BuildContext context, int index) {
                final todo = todoList[index];
                return Row(
                  children: [
                    Expanded(
                      child: Row(
                        children: [
                          Checkbox(
                            value: todo.completed,
                            // チェックボタンの更新。
                            onChanged: (value) => todoNotifier.toggle(todo),
                          ),
                          // completedがtrueの場合は文字に横線を入れ、色はグレーにする。
                          Text(
                            todo.title,
                            style: TextStyle(
                              color:
                                  todo.completed ? Colors.grey : Colors.black,
                              decoration: todo.completed
                                  ? TextDecoration.lineThrough
                                  : TextDecoration.none,
                            ),
                          ),
                        ],
                      ),
                    ),
                    IconButton(
                      // Todoの削除
                      onPressed: () => todoNotifier.remove(todo),
                      icon: const Icon(Icons.delete),
                    ),
                  ],
                );
              }),
            ),
          )
        ]),
      ),
    );
  }
}

コンソール結果

todoNotifier.add(controller.text);を実行すると、一つの処理が終わるまで次の処理が実行されていないことが確認できます。

まとめ

今回は簡単なTodoの例を実装しました。NotifierProviderを使用する場合、処理の種類や目的に合わせて適切な処理方法を選択することが重要です。ぜひ利用してみてください。

採用情報はこちら
目次