LoginSignup
3
5

More than 1 year has passed since last update.

Flutterの状態管理パッケージとその使い方

Posted at

はじめに

これらが個人的必須級のパッケージで、これらの話をします。
もう StatefulWidgetを使うのはやめましょう

この記事では以下のバージョンあたりの話をすることを前提とします。

pubspec.yaml
    flutter_hooks: ^0.18.5+1
    freezed: ^2.2.0
    hooks_riverpod: ^2.0.2

本文

hooks_riverpod

画面間で共有のステートを定義する際に使用しているパッケージです
使う際には以下のように main.dart の MyApp (一番親のwidget)を ProviderScope で囲みます

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

例えば int 型の値を複数画面間で共有したい場合は以下のようになります。

sample.dart
    final myStateProvider =
        StateNotifierProvider<MyStateNotifier, int>((ref) => MyStateNotifier());
    
    class MyStateNotifier extends StateNotifier<int> {
      MyStateNotifier() : super(0); // super()で初期値を設定する
      void increment() {
        state++;
      }
    }
    
    class SampleView extends ConsumerWidget { // ConsumerWidget を継承した例
      Widget build(BuildContext context, WidgetRef ref) {
        return Scaffold(
            body: Center(child: Text(ref.watch(myStateProvider).toString())));
      }
    }

    class SubSampleView extends StatelessWidget { // Consumer を使用した例
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: Consumer(
                    builder: (context, ref, child) =>
                        Text(ref.watch(myStateProvider).toString()))));
      }
    }

参照する側の Widget は ConsumerWidget を継承することで build の 引数に WidgetRef が追加され、ref.watch()を使用することで中身の値を参照することができます。
もしくは StatelessWidget の中で Consumer を使用して参照します。
ここで注意なのですが、ConsumerWidget や Consumer はできるだけ小さな範囲で使用してください。
大量の widget を持つ widget で ConsumerWidget を継承して使用すると 値が更新されるたびに全体を再描画しようとしてしまいます。パフォーマンスの観点から最悪です。また、Consumer を使用する際も同様で、内部の widget に状態とは関係ないものがないのが理想です。

ちなみにですが、例における int 部分は自作のクラスを使用して実装することも当然できます。
しかし state は immutable なので class などを使用している場合、メンバを直接編集することはできません。新しくコンストラクタ等で再初期化しなければ正しく状態が更新されることはありません。
(しかし、いちいちコンストラクタで生成するのは面倒です。これを解決するのが freezed パッケージです。後述します。)

値を更新する側は以下のようになります。

sample.dart
    class StateChangerView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: Consumer(
                    builder: (context, ref, child) => TextButton(
                        onPressed: () {
                          ref.read(myStateProvider.notifier).increment();
                        },
                        child: const Text('更新するボタン')))));
      }
    }

ボタンを押されると increment() が実行され、myStateProvider で提供される値が更新されます。
その後 ref.watch で監視していた widget が rebuild されます。

こうして複数の画面から同じ値を参照したり更新したりできます。
ただ、全ての状態が画面間で共有される必要はありません。
一つの画面だけで十分な情報も必要でしょう。
その際に使うのが flutter_hooks です(だと思います)

flutter_hooks

flutter_hooks はひとつの画面で完結する状態を持つ際に使うと良いでしょう。
使い方は簡単で

sample.dart
     class SampleView extends HookWidget {
       @override
       Widget build(BuildContext context) {
         final xx = useState(0);
         return Scaffold(
           body: Center(
               child: Column(
             mainAxisAlignment: MainAxisAlignment.center,
             mainAxisSize: MainAxisSize.max,
             children: [
               Text(xx.value.toString()),
               TextButton(
                   onPressed: () {
                     xx.value++;
                   },
                   child: const Text('更新するボタン')),
             ],
           )),
         );
       }
     }

useState()を用いて状態を生成し、.value でアクセスします。
他にも useTextEditingController() などがあり、TextEditingController のような dispose を記述しなければならないものを省略できます。
様々なものがあるので公式 https://pub.dev/packages/flutter_hooks で確認しましょう。

freezed

mvvm などの model 部分で使われることが多いと思いますが、immutable なクラスの生成を楽にしてくれるものです。
hooks_riverpod にて state は immutable なものであり、クラスのメンバの直接編集はできないと書きました。
これを解決するための freezed パッケージです。
まずは以下を実行してパッケージを追加します
$ flutter pub add build_runner --dev

次に必要なクラスを定義します。
例えば counter という変数を持つ SampleClass を定義したい場合は、以下のようなファイルを書きます。

sample.dart
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:flutter/foundation.dart';
    part 'sample.freezed.dart';
    
    @freezed
    class SampleClass with _$SampleClass {
      const factory SampleClass({
        @Default(0) int counter,
      }) = _SampleClass;
    }

恐らくエラーが多く出ているはずですが、そのままで構いません。
$ flutter pub run build_runner buildを実行します
新しくファイルが生成された後エラーが解決されていれば問題ありません。
元ファイルが sample.dart の場合 sample.freezed が生成されているでしょう。
これで準備は完了です。
あとは好きな所で

sample.dart
    final tmp = SampleClass(counter: 0);
    final tmp2 = tmp.copyWith(counter: 10);

このように copyWith を用いて immutable なオブジェクトを生成できます。
StateNotifier で使うなら以下のようになります。

sample.dart
    class MyStateNotifier extends StateNotifier<SampleClass> {
        MyStateNotifier() : super(const SampleClass());
        void increment() {
            state = state.copyWith(counter: state.counter + 1);
        }
    }

あまり恩恵を受けられていないように感じますが、
例えば SampleClass にメンバが10個ほどあったとしたら、一つのメンバを更新するだけで

sample.dart
    void increment() {
        state = SampleClass(
            counter: state.counter + 1,
            hoge: state.hoge,
            fuga: state.fuga,
            piyo: state.piyo,
            foo: state.foo,
            bar: state.bar,
            //......
        )
    }

となり、それぞれの状態に対して個別で更新するメソッドを実装するとなった時に無駄に長いコードになってしまいます。

また、$ flutter pub add json_serializable --devを実行した後

sample.dart
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:flutter/foundation.dart';
    part 'sample.freezed.dart';
    part 'sample.g.dart';
    
    @freezed
    class SampleClass with _$SampleClass {
      const factory SampleClass({
        @Default(0) int counter, // Add your fields here
      }) = _SampleClass;
      factory SampleClass.fromJson(Map<String, dynamic> json) =>
          _$SampleClassFromJson(json);
    }

のように
part 'sample.g.dart'factory SampleClass.fromJson(Map<String,dynamic> json) => _$SampleClassFromJson(json);
を追加し、build_runner を実行し直すと json との相互変換も可能になります。

sample.dart
    SampleClass tmp = const SampleClass();
    tmp.toJson(); // -> Map<String, dynamic>
    SampleClass.fromJson({'counter': 0}); // -> SampleClass(counter: 0)

データベースとの通信をする場合、多くは json の状態でやり取りされるでしょう。
データを受け取ってそのままクラスに変換可能で、データベースに送信する際にもJson変換が便利です。

さいごに

新旧入り混じって様々な状態管理手法が蔓延っている中、現時点での個人的な最終見解です。
紹介したものは基本的な部分しか書いておらず、riverpod v2 の AsyncValue など便利そうなものも整理を兼ねて書きたかったのですが長くなりそうなのでここでは紹介しません。

まとめ:StatefulWidget は使わない

3
5
0

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
5