Flutter#2 〜Flutterの状態管理パターン (Riverpod)〜

はじめまして、Advance Tech Divisionでモバイルエンジニアをしている中塚です。今回はFlutterシリーズの第2弾ということで、Flutterでの状態管理パターンについて書こうと思います。Flutter での状態管理は様々なパターンが存在しており、当社でもプロジェクトによって利用しているパターンが異なっております。その中で、今回は状態管理で最近よく使われているRiverpodについて記載していきたいと思います。第1弾の記事については、こちらを参照ください。

Flutter#1 〜ARISE analyticsのプロジェクトでFlutterを採用してみました〜 | 株式会社ARISE analytics(アライズ アナリティクス) )

状態管理パターンとは?

以下の動作を管理することを状態管理パターンといいます。

  • 別々の画面で状態(値)を共有することができる
  • 状態の変更に応じて、UIが更新される

 Flutterでの状態管理パターン

現在Flutterでは、全てではありませんが以下のような状態管理パターンが使われております。今回はその中でも比較的新しくて、最近よく使われている StateNotifier + freezed + Riverpod + FlutterHooks のパターンについて紹介していきます。

  • StatefulWidget
  • InheritedWidget
  • BLoC + Provider
  • ChangeNotifier + Provider
  • StateNotifier + freezed + Provider
  • StateNotifier + freezed + Riverpod + Flutter Hooks (今回の紹介内容)

StateNotifier

ChangeNotifierでは、状態の変更を通知するために、状態を変更するたびに notifylisteners関数を呼ぶ必要があります。この煩わしさを解消してくれたのが、StateNotifierになります。

StateNotifierは、一つの状態(state)しか持つことができません。そのため、複数の状態を管理したい場合は、オブジェクトを作成して管理することになります。オブジェクトに作成については、後述するfreezedを参照ください。

下記は、カウンターの例になります。stateとしてint型を宣言し、0で初期化します。increment関数が呼ばれるたびにstateに1が足され、自動的にリスナーに変更が通知されます。

※使用している言語はdart

class CounterStateNotifier extends StateNotifier<int> {
  CounterStateNotifier(): super(0);

  void increment() {
    state++;
  }
}

freezed

StateNotifierで利用するState用のクラスをimmutableにすると、StateNotifier側の記述が冗長になってしまいます。これを解決するのfreezedパッケージになります。(freezed – Dart API docs

State用に作成したimmutableなクラスの例になります。

@immutable
class CounterState {
  CounterState({
    this.count = 0,
    this.isEnabled = true,
  });
  final int count;
  final bool isEnabled;
}

StateNotifierでStateを変更するたびにオブジェクトを再作成する必要があり、countのみを更新したいのにisEnableも定義する必要があり、記述が冗長になってしまいます。

class CounterStateNotifier extends StateNotifier<CounterState> {
  CounterStateNotifier(): super(CounterStateNotifier());

  void increment() {
    state = CounterState(
      count: state.count + 1,
      isEnabled: state.isEnabled,
    );
  }

  void disableCounter() {
    state = CounterState(
      count: state.count,
      isEnabled: false,
    );
  }
}

 

freezedは、後述するRiverpodの作者が作成したパッケージになります。freezedを利用することで様々な関数を自動生成してくれます。これらを利用することで、冗長なコードが簡潔になります。

  • copyWith
  • Jsonのパース
  • ==
  • toString()
  • 遅延初期化など

freezedを利用したState用のクラス例になります。

@freezed
class CounterState with $_CounterState {
  factory CounterState({
    int? count,
    bool? isEnabled,
  }) = _CounterState;
}

ターミナルで以下のコマンドを実行すると、@freezedが付ているクラスを自動で検出し、コードを自動で生成してくれます。

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

freezedを用いた場合のStateNotifierは、先ほどのコードに比べると記述が簡潔になっていることが分かると思います。

class CounterStateNotifier extends StateNotifier<CounterState> {
  CounterStateNotifier(): super(CounterState(count: 0, isEnabled: true));

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }

  void disableCounter() {
    state = state.copyWith(isEnabled: false);
  }
}

Riverpod

Riverpodとは、状態管理パッケージで、Providerと同じ開発者が作成したものになります。Providerの欠点を補った改良版のProviderです。Riverpodという名前もProvierのアナグラムになっております。(Provider, but different | Riverpod

Riverpodは、3種類あります。Riverpodの開発者は、hooks_riverpodの利用を推奨しております。hooks_riverpodで利用するFlutter Hooksについては、後述で説明します。

 

Riverpodのメリット・デメリットとしては以下があげられます。

Flutter Hooks

React HooksのFlutter版になります。こちらもRiverpodの開発者が作成しており、Riverpodとの併用を推奨しております。Hook Widgetを継承することで、便利なuseXXX関数を利用することができます。(flutter_hooks – Dart API docs

Flutter HooksとRiverpodを組み合わせることで、state取得時にselectを利用することができます。

Flutter Hooks を使わない場合、ConsumerWidgetを継承するか、Consumerを利用することになります。

final provider = StateNotifierProvider<CounterStateNotifier, CounterState>((ref) => CounterStateNotifier());

class CounterWidget extends ConsumerWiddget {
  @override
  Widget build(BuildContext, context, ScopedReader watch) {
    final CounterStateNotifier notifier = watch(provider.notifier);
    final CounterState state = watch(provider);  
  }
}

FlutterHookを利用する場合、HookWidgetを継承してuseProviderを利用することになります。状態が変更された際に自動で再ビルドされます。

final provider = StateNotifierProvider<CounterStateNotifier, CounterState>((ref) => CounterStateNotifier());

class CounterWidget extends HookWidget {
  @override
  Widget build(BuildContext, context) {
    final CounterStateNotifier notifier = useProvider(provider.notifier);
    final CounterState state = useProvider(provider);  
  }
}

おわりに

今回はFlutterの状態管理パターンであるStateNotifier + freezed + Riverpod + Flutter Hooks について紹介しました。今回の例は、一つのパターンに過ぎず、今後多くのパターンが登場されると思います。その中でお気に入りのパターンを見つけていただければと思います。