[Flutter] Redux 入門
ReduxはFlutterで用いられる状態管理手法の一つです。元々はReactで使用されている状態管理を行うためのライブラリで、各コンポーネントに対して簡単に状態 (State)を共有することができます。Reduxの考え方はFlutterにも適用することができ、各ウィジェットに対して状態 (State)を共有することができます。
Reduxの3原則
Reduxには守らないといけない3つのルールがあります。
- 状態 (State)は一つのオブジェクトに集約する。
アプリの状態を一つのオブジェクトに集約することで、状態の管理をシンプルにすることができます。 - 状態 (State)は読み込み専用にする。
アプリケーションの状態を読み込み専用にすることで、予期せぬ状態の変更を防ぐことができます。 - 状態 (State)の変更は関数を通してのみ行う。
アプリケーションの状態変更は関数を通してのみ行うことで「2」を実現することができ、状態の更新経路を一つの関数にすることで、状態変更経路をシンプルにすることができます。
Reduxの構成要素
ReduxではStore、Action、Reducerという三つの要素が重要な役割を果たします。
Store
Reduxでは「Store」と呼ばれるオブジェクトの中で、アプリケーションの状態を管理します。また、「Store」はアプリケーション内に一つだけ存在します (原則1)。
Action
画面に表示されるボタンがタップされるといったイベントが発生するとView (画面)はActionというオブジェクトを作成します。このオブジェクトを後述するReducerに渡します。
Reducer
Actionの内容に基づいてReducerが新しい状態を作成し、Storeで管理している状態に反映します (原則3)。状態は読み込み専用となるため (原則2)、現在の状態に変更を加えることはせず、状態を新しく作成します。
Reduxでの状態変更フロー
Reduxでの状態変更は単一方向に行われます。
- View (画面)でイベントが発生
- View (画面)で発生したイベントに対応するActionを生成
- ActionをReducer関数に送信
- Reducer関数はActionを解析して状態 (State)を更新
- 状態の変更を画面に反映
実装
カウンターアプリをReduxを用いて開発してみます。画面中央にカウント数を表す数字が二つ、カウント数をインクリメント/デクリメントする為のボタンが四つ表示されるシンプルなアプリです。
ソースコードは以下に置いています。
パッケージのインストール
flutter_reduxパッケージをインストールするため、pubspec.yamlに以下のコードを記述します。
Stateクラスの作成
カウント数を保持するためのint
型変数counterA、counterBを定義しています。読み込み専用にするため、final
修飾子を付加しています。
今回のアプリケーションではStateクラスは一つのみですが、実践的なアプリケーションを開発する時は、Stateクラスが複数になってきます。Stateクラスが複数になった時に対応するため、Stateクラスを一度にまとめて初期化するためにRootState
というクラスを用意します。RootState
クラスでは全てのStateクラスをメンバ変数として保持します。
Actionクラスの作成
Actionクラスは画面で発生したイベントを区別するためのクラスです。今回用意したイベントは以下の四つです。
- CounterAをインクリメント
- CounterAをデクリメント
- CounterBをインクリメント
- CounterBをデクリメント
画面で発生したイベントを区別するためのクラスなので、クラス内は空で問題ありません。
Reducer関数の作成
Stateクラスを更新するReducer関数を作成します。Reducer関数は (元の状態(State), アクション) => 新しい状態(State) といった形式の純粋関数にします。 引数のアクションに対して、アクションの種別を判定して、新しい状態を生成しています。
StoreProviderウィジェットの配置
Store
クラスオブジェクトをWidgetツリー全体で使用することができるようにするために、flutter_reduxプラグインで提供されているStoreProvider
ウィジェットを上位ツリーに配置します。
StoreProvider
ウィジェットは下位ツリーのウィジェットにStore
クラスオブジェクトを渡せるようにするためのウィジェットです。store
引数にはStore
クラスオブジェクトを指定し、child
引数には表示したいウィジェットを指定します。
今回はMyHomePageウィジェット以下でStore
クラスオブジェクトを参照できるようにしています。
下位ツリーからStoreクラスオブジェクトにアクセス
下位ツリーからStore
クラスオブジェクトにアクセスする手段はStoreBuilder
ウィジェットを使用する方法とStoreConnector
ウィジェットを使用する方法があります。
StoreBuilder
StoreBuilder
ウィジェットの引数に指定したビルダー関数の引数からStore
クラスオブジェクトにアクセスすることができます。Store
クラスオブジェクトに変更が入った時、StoreBuilder
ウィジェットの引数に指定したビルダー関数はリビルドされます。つまりStore
クラスオブジェクトに変更が入った時 (= counterA、counterBのいずれかに変更が入った時)に、ビルダー関数で生成するウィジェットの表示を更新することができます。サンプルコードではStoreBuilder
ウィジェットの引数に指定したビルダー関数でWidgetAを生成するようにしています。
StoreConnector
StoreConnector
ウィジェットは特定の状態が変更された時、StoreConnector
ウィジェットの引数に指定したビルダー関数をリビルドさせることができます。StoreConnector
ウィジェットのconverter
引数には、引数にstore
を渡して、変更を検知したい変数を返す関数を指定します。さらに、ここで指定した関数の返り値はbuilder
引数に指定したビルダー関数の引数となります。
サンプルコードではconverter
引数に指定した関数の返り値をcounterBにすることで、builder
引数に指定したビルダー関数の引数にcounterBが渡され、counterBに変更が入った時のみ、StoreConnector
ウィジェットの引数に指定したビルダー関数をリビルドさせるようにしています。サンプルコードではStoreConnector
ウィジェットで指定したビルダー関数でWidgetBを生成するようにしています。
実行結果
ボタンをタップする度に画面中央に表示されている数字がインクリメントされています。
[Increment Count A] / [Decrement Count A]ボタンをタップした時は、コンソールに'Build WidgetA'が表示されています。これはCounterStateオブジェクトのcounterA変数が変更されたことをStoreBuilder
ウィジェットが検知し、ビルダー関数が実行された結果WidgetAがリビルドされていることが理由です。また、counterA変数に変更が入ってもコンソールに'Build WidgetB'が表示されず、WidgetBはリビルドされていないことがわかります。
[Increment Count B] / [Decrement Count B]ボタンをタップした時は、コンソールに'Build WidgetA'、'Build WidgetB'が表示されています。これはCounterStateオブジェクトのcounterB変数が変更されたことをStoreBuilder
ウィジェット、StoreConnector
ウィジェットが検知し、両ウィジェットのビルダー関数が実行された結果、WidgetA、WidgetBがリビルドされていることが理由です。
Discussion