3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Flutter状態管理 Riverpod(StateNotifier) x Freezed

Last updated at Posted at 2022-06-08

Flutterにおける状態管理

RiverpodFreezedを用いた状態管理まとめ

想定読者

  • Flutterの最低限の知識がある
  • Providerにおける状態管理の構造を理解している
    必ずしもProviderを理解しておく必要はないが、Providerを理解していると、よりRiverpodも理解しやすくなる
  • RiverpodでstateProviderでの状態管理を理解している。

その上で、StateNotifierの理解に苦しんでいる方

導入方法

Riverpodの導入
Riverpodの導入方法はRiverpod公式Documentを参照してください。
pubspec.yamlを編集してもいいが、以下のようにターミナルで行うと便利

$ flutter pub add 追加したいパッケージ名

Freezedの導入
importするのはfreezed_annotaionのみで、下2つはbuildするときに必要となる。

$ flutter pub add freezed_annotation
$ flutter pub add --dev freezed
$ flutter pub add --dev build_runner

StateNotifier における状態管理の構造

大きく分けて3つのclassを用いる

  • 状態変数をまとめたclass
  • 状態の操作をするclass
  • UIから状態にアクセスすることを可能にするclass

これらを簡単にまとると以下のようになる

  1. 状態変数をまとめたclass
    このclass内で状態変数を定義する。このclassは状態変数の保守性を高めるためにimmutableに宣言する。immutableであるが、もちろんイベント(2で定義する)を応じてかえることができる。
  2. 状態を操作をするclass
    このclassでは1で定義した状態変数を変更するメソッドを定義する。
    StateNotifierを継承する。
    stateで、1で定義された状態変数にアクセスすることができる。
    changeNotifierとは異なり値の変更を通知する際に、NotifyListers()が不要で
    stateが更新されると自動的に値の変更を通知する。
    1で書いたように、stateはimmutableであるため、freezedパッケージを持ちいない場合は新たに1のclassを作り直す必要があり、コードが冗長になる。
  3. UIから状態にアクセスすることを可能にするclass
    StateNotifierProviderを継承する。
    UIから状態にアクセスする際には、このclassを通じて行う

実装

例としてユーザー情報を管理するStateNotifierを作ってみる。

Providerの実装

管理する状態変数は

  • UserName
  • Email
  • age

の3つをとする。

userdataProvider.dart
import 'package:freezed_annotation/freezed_annotation.dart';

// ファイル名.freezed.dart
part 'userdataProvider.freezed.dart';

// 以下で示すbuildをするまではErrorが出ると思うが、無視して良い。
// 状態変数を定義するclass
@freezed
// _$UserDataをmixinし、コンストラクタに_UserDataを代入する
class UserData with _$UserData {
  const factory UserData({
    // @Default(初期値) 型 変数名
    // @Defaultは不要であるが、NullSafety対策にもなる
    @Dafault('') String userName,
    @Default('') String email,
    @Default(0) int age,
  })  = _UserData;
}

次にGeneratorを走らせる。UserData.freezed.dartというファイルが生成されエラーが解消される。
(UserData classに変更を加える度にこのコマンドを走らせる必要がある)
Freezedに関連するエラー以外がある場合にはうまくいかない。

terminal
$ flutter pub run build_runner build --delete-conflicting-outputs

次に状態を操作をするclassは次のようになる

userdataProvider.dart
// StateNotifierを継承する
class UserDataNotifier extends StateNotifier<UserData>{
  // 初期化
  UserDataNotifier() : super(const UserData());

  // 状態を変更するメソッド
  // Usernameを変更するメソッド
  void changeUserName(String newUserName) {
    // freezedパッケージによりcopywithが使える。
    // stateはimmutableであるが、copywithを使うことで、簡単に書ける。
    state = state.copywith(userName: newUserName);
  }

  // 年齢を変更するメソッド
  // 上と同様に書ける
  void changeAge(int newAge) {
    state = state.copywith(age: newAge);  
  }

  // ...
}

最後にStateNotifierProviderの実装

userdataProvider.dart
// providerはglobalで宣言 immutableのため特に気にする必要はない
// StateNotifierProvider<2のclass, 1のclass>
// return 2のclass
final userDataProvider = StateNotifierProvider<UserDataNotifier, UserData>((ref) {
  return UserDataNotifier();
});

UIに表示させる

次にUI側で状態変数にアクセスしたり、状態変数を変更したりする部分の実装。
そのためには、ref オブジェクトを参照する必要があり、そのためのWidgetが以下のようにriverpodには用意されている。

Flutter Riverpod
StatelessWidget ConsumerWidget
StatefulWdiget ConsumerStatefulWidget
State ConsumerState

これらのWidgetは、従来のFlutterのWidgetと大差はないが、buildメソッドに対してBuildContextに加え、WidgetRefを引数として追加する。

また、ConsumerというWidgetが用意されており、refオブジェクトに参照するためにclassを作らなくてもよい。

ref.watch, ref.read, ref.listenについても触れておく

  • ref.watch
    状態変数を監視し、変化するたびにWidgetをRebuildする。
    initStateや、onPressed内といった非同期処理ではwatchは使われるべきではなく、その代わりにref.readを使う
  • ref.read
    状態変数は監視しないが、その時点でのstateを取得することができる。
    普通、ボタンが押されたときの処理などに使われる。
    公式documentによると、ref.readはできるだけ使用を避けるべきとのこと。
  • ref.listen
    基本はref.watchと同じであるが、ref.listenは任意の関数を呼び出すことができる
    注)今回は扱わない

以下では、ConsumeWidgetを継承したUserNameを変更するclassと、Consumerを使った年齢を表示するWidgetである。

main.dart
class UserSettings extends ConsumerWidget {
  const UserSettings({Key? key}) ; super(key:key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children:[
        // ...
        TextFormField(
          // ...
          onChanged:(String value){
              // 値を変更するときは、次の様に行う
              // ref.read(StateNotifierProviderを継承するclass.notifier).メソッド
              ref.read(UserDataProvider.notifier).changeUserName(value);
            },
           // ...
        ),
        // ...
      ],
    );
  }
}
main.dart
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) ; super(key:key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:Center(
        child:Column(
          children:[
            // ...
            Consumer(
             builder: (context, ref, child) {
               return Text(
                 // 状態変数にアクセスしたいときは、
                 // ref.watch(Provider).状態変数名
                 // 値が変更されたときにrebuildされるのはConsumer内のWidgetである、
                 // できるだけConsumerは小さく。
                 ref.watch(userDataProvider).userName,
               );
             }
            ),
            // ...
          ],
        );
      );
    );
  }  
}

参考文献

Riverpod公式Document

freezed Dart Package

まとめ

自分が、StateNotifierを理解するのに苦労したため、まとめてみた。
StateNotifierProviderが自分で実装できるようになると、より複雑な状態管理が必要になっても、シンプルな形で書けるようになり非常に便利であると感じた。

今後、さらにhooksを加えて、
Riverpod x Freezed x Hooks
などで状態管理もしてみたい

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?