Tags: Flutter

Flutter の Riverpod による状態管理

かつて BLoC パターンが使われていた時代から Flutter について全くキャッチアップしていなかったので Riverpod による状態管理をやる。

開発環境

  • Flutter SDK: 3.0.4
  • flutter_riverpod: 2.0.0-dev.9
  • freezed: 2.0.4

本文

自分用に作りたいアプリがあり、Flutter でサクッと作ろうと思ったが知識が古すぎるのでキャッチアップすることにした。

まずそもそも Flutter を忘れていたので

を参考にして
StatefulWidget を使用する例を実際に書く
→ Riverpod を使用する例に書き換える
という順序でざっくり概念を掴んだ。

実際に書いてみた感想として、BLoC パターンよりかなり記述量が少なくて済むという印象である。

これ以降はその後実際に作りたい物を作っていったソースコードを引用する。

インストール

まず Riverpod の公式サイト を見てパッケージをインストールする。

今回は freezed も併用するので freezed もインストールする。

main.dart

lib/main.dart で

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

このように ProviderScope で全体を囲む。

State の定義

どこでもいいので state を定義するファイルを作る。
私は lib/states/states.dart にした。

以下のように state を定義する。

import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'states.freezed.dart'; // ここの states はファイル名と同じにする

@freezed
class AddPageState with _$AddPageState {
  const factory AddPageState({
    required DateTime date,
    required TimeOfDay time,
    required String event,
    required String thoughts,
    required String emotion,
    required int intensity,
    required String action,
    required String result,
  }) = _AddPageState;
}

作ろうとしているのはアンガーログというやつで、この state はフォームに入力する項目である。
書いた時点ではエラーが出ているので、以下のコマンドをターミナルで実行する。

flutter pub run build_runner build

この結果、lib/states/states.freezed.dart というファイルが自動生成されてエラーが消える。

なお上記のコマンドは @freezed アノテーションがついているファイルを見つけてコードを自動生成してくれるのだが、複数のファイルに書いてあると最初の一つしか見つけてくれない(?)ようだった。
1 ファイル内に複数の @freezed が書いてあるのは問題なかったので、定義を 1 ファイルに纏めれば問題ない。

Riverpod

アンガーログの入力フォームを作る。

lib/pages/add.dart を作成する。
ソースコードが長いので全文は GitLab を参照。

ポイントとしては、まず Notifier と Provider を作る。

class AddPageStateNotifier extends StateNotifier<AddPageState> {

  AddPageStateNotifier()
      : super(AddPageState(
            date: DateTime.now(),
            time: TimeOfDay.now(),
            event: '',
            thoughts: '',
            emotion: '',
            intensity: 5,
            action: '',
            result: ''));


  void setDate(DateTime date) {
    state = state.copyWith(date: date);
  }

  // 中略

  void save() {
    print(state);
  }
}


final addPageProvider =
    StateNotifierProvider<AddPageStateNotifier, AddPageState>((ref) {
  return AddPageStateNotifier();
});

なお Notifier と Provider はどこからでも読み込むことができるので、定義するファイルはどこでもいい。

次に Provider を使用するには ref を取得する必要があるが、幾つも方法があるので公式サイトを読んで適切な方法を使う。

プロバイダの利用方法

参考にしたソースコードは StatelessWidget + Consumer という方法にしていたが、ConsumerWidget を使うほうがネストが浅くていいと思う。

class AddPage extends ConsumerWidget {

  const AddPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
        appBar: AppBar(title: const Text('新規登録')),
        body: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,

            children: <Widget>[
              DatePickerParts(firstDate: DateTime(2022), lastDate: DateTime.now()),
              const TimePickerParts(),
              const EventParts(),
              const ThoughtsParts(),
              const EmotionParts(),
              const IntensityParts(),
              const ActionParts(),
              const ResultParts(),
              ElevatedButton(
                child: const Text('保存'),

                onPressed: () {
                  ref.read(addPageProvider.notifier).save();
                  const snackBar = SnackBar(
                    content: Text('保存しました'),
                    backgroundColor: Colors.green,
                  );
                  ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  Navigator.pop(context);
                },
              ),
            ],
          ),
        ));
  }
}

ref.read を使うとその時点での state が取れる。たとえば下記のように使う。

ref.read(addPageProvider.notifier).save();

ただし ref.read を使ってプロバイダのステートを取得する によると

ref.read はリアクティブではないため、可能な限り使用を避けてください。

watch や listen の使用では問題が生じる場合の回避策として存在しています。 ほとんどの場面では watch や listen の使用、特に watch の使用がベターなはずです。

とのこと。

ref.watch を使ってプロバイダを監視する のほうは

// 全部取る
final state = ref.watch(addPageProvider);
// 特定の値だけ取る
final time = ref.watch(addPageProvider.select((state) => state.time));

のようにして state の変更を読み出せる。

こちらにも注意書きがあって

watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。

これらの場合は代わりに ref.read を使用してください。

とのこと。

要するに定義したアクションを実行するときは read で、値を画面に表示するときは watch ということでいいと思う。

本日やったのはここまで。