はじめに
Flutterに関わらずSPAやモバイル開発をすると、アプリケーションの状態管理をどうしようかと悩む時があります。もし状態管理がないまま開発をすると、各ページに状態を引き継ぐ処理が必要になります。(とっても面倒臭い..!)
そんな状態をアプリケーションの中で集中管理できれば、状態を引き継ぐ処理などが減り、開発が効率的に行えます。
Reduxについて
状態管理をするためのアーキテクチャーとして、Reduxがあります。reduxはよくreactと一緒に使われることが多いんですが、Flutterでもreduxが使えます。
主な登場人物は
- Action
- Reducer
- Store
- Middleware(使うかどうか自由)
です。
色々調べているとこんな図をよく見かけました。データフローは単方向なんですね。
Actionは処理名の定義です。
実際の処理はReducerが担います。Reducerは前の状態を受け取り新しいものを作るという副作用のない関数になります。(例:Aを入れたら毎回Bが返る)
Storeがアプリケーション全体の状態(state)を保持している役割をします。Store内の状態はReducer経由でないと変更できません。
では副作用があるものはどうしたらいいのでしょうか。例えばアプリケーションで必ず発生する外部APIは副作用のあるものとされます。そのような外部APIとの連携はMiddlewareと呼ばれる部分が担当します。MiddlewareはReducerの前後に置けます。
と色々なサイトで見たきた内容を簡単にまとめましたが、理解するにはやはりコードを書いた方が早いのでコードを書いてみます。
実際に実装してみると下図のようイメージになります。Storeを通して色々とやりとりすることになります。
Flutterで実装
ここではカウントアップを例に実装してみます。
Flutterプロジェクトの作成
Flutterプロジェクトを作成します。
$ flutter create learn_redux
今回はとても簡単なプログラムなので、全てmain.dartに実装していきます。本当はフォルダ分けして管理する方がよいと思います。
Reduxパッケージの導入
pubからReduxパッケージを落としてきます。
各バージョンはpubでそれぞれのページから確認ください。
redux
flutter_redux
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
redux: ^3.0.0 # Add
flutter_redux: ^0.5.2 # Add
yamlに書いたら次のコマンドを実行します。
$ flutter packages get
これで準備OKです。
状態クラス(AppState)を作成
Storeが保持するstateクラスをまず実装します。このクラスはイミュータブル(不変)なクラスで作成します。AppStateという名前がデファクトのようです。
import 'package:meta/meta.dart';
@immutable
class AppState {
final int counter;
AppState(this.counter);
}
ここではcounterというプロパティを持ったクラスを定義します。コンストラクタに整数を受け取る形です。このような形で状態管理したい項目をプロパティに持つような形をとります。
Actionの定義
次にActionの定義です。ActionはReducerを実行する処理名を定義するような位置付けと冒頭に書きました。classやenumの形どちらでもよいですが、今回はenumで作りました。先ほどのAppStateクラスの下あたりに書きます。
enum Actions { Increment }
Reducerの定義
Reducerは実際に処理をするところでした。その処理というのはActionで定義されている処理しか行いません。
reducerは引数が二つあります。1つ目が処理する前の状態AppStateのインスタンスです。第2引数がActionです。
今回はActions.IncrementというActionの時しか処理しないためif文処理を書きます。返り値は新しいAppStateインスタンスです。
冒頭でReducerは副作用のない関数と話しましたが、古い状態を入れると新しい状態が返ってくるという副作用のない関数になっていることがわかります。(古い状態を直接書き換えることはしない!)
定義しているAction以外のものは前の状態そのままを返すようにします。
AppState reducer(AppState prev, action) {
if (action == Actions.Increment) {
return AppState(prev.counter + 1);
}
return prev;
}
main.dartを完成させる
画面側を一気に書きます。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
Store<AppState> store = Store(reducer, initialState: AppState(0)); // ※1
@override
Widget build(BuildContext context) {
return StoreProvider( // ※2
store: store,
child: Scaffold(
appBar: AppBar(
title: Text('Learn Flutter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StoreConnector( // ※3
converter: (Store<AppState> store) => store.state.counter,
builder: (context, counter) => Text(
'$counter',
style: Theme.of(context).textTheme.display1,
),
),
],
),
),
floatingActionButton: StoreConnector( // ※4
converter: (Store<AppState> store) {
return () => store.dispatch(Actions.Increment);},
builder: (context, callback) => FloatingActionButton(
onPressed: callback,
tooltip: 'Increment',
child: Icon(Icons.add),
)
), // This trailing comma makes auto-formatting nicer for build methods.
));
}
}
Reduxが関わるところだけを確認していきます。
まずは下記の部分
Store<AppState> store = Store(reducer, initialState: AppState(0));
ここでは一番最初の状態を作るところです。Storeが全ての状態や処理を牛耳っているので、そのStoreを作成するところから始まります。渡すのは処理するためのreducerとAppStateの初期状態を渡します。
Widget build(BuildContext context) {
return StoreProvider( // ※2
store: store,
child: Scaffold(
appBar: AppBar(
title: Text('Learn Flutter'),
),
//////////////////////////////////// 省略
floatingActionButton: StoreConnector(
converter: (Store<AppState> store) {
return () => store.dispatch(Actions.Increment);},
builder: (context, callback) => FloatingActionButton(
onPressed: callback,
tooltip: 'Increment',
child: Icon(Icons.add),
)
),
//////////////////////////////////// 省略
お次はこちらのStoreProviderです。ここでは先ほど作ったStoreを設定して、これ以降の画面でstoreを共有できるようなものと思ってもらったらいいと思います。
store引数に初期状態を設定して、あとchildにはいつも通り色々なウィジェットを追加してください。
これでreduxによる状態管理が始まりました。
では実際に状態を取得したり使ったりするにはどうしたらいいんでしょうか?
それはStoreConnectorです
StoreConnector( // ※3
converter: (Store<AppState> store) => store.state.counter,
builder: (context, counter) => Text(
'$counter',
style: Theme.of(context).textTheme.display1,
),
),
converterではStoreを引数としてもつ関数を定義して挙げます。返り値はなんでもいいんですが、次の行のbuilderという引数も同様に関数を持たせるんですが、その第二引数にconverterで作った情報が引き継がれます。
converterからbuilderに値を渡すことができるので、関数やインスタンスも同様に渡すこともできます。
そしてStoreにはdispatchというメソッドを持っています。これにActionで定義している定義名を渡すことでそのActionに沿った処理を行ってくれます。
builderの返値はWidgetなので、ここではいつも通り好きなUIを作って挙げたらOKです!
Storeの中身を使いたい時はStoreConnectorの中に実装するということを覚えておけば良さそうですね。
ビルドしてみましょう!
おおーちゃんと動いている!!(感動)
これでReduxを使った基本的なデータの流れがわかりました。あとはmiddlewareとかいうやつです。
middlewareの定義
middlewareは第一引数にStore、第二引数にAction、第三引数にNextDispatcherという形の関数を自作します。
void middleware(Store<AppState> store, action, NextDispatcher next) async {
if(action == Actions.Increment) {
print('今は${store.state.counter}です。+1するよ');
}
}
試しにこれでやってみましょう。middleware関数を追加したので、Storeに登録する必要があります。Storeを作成するところで引数でmiddlewareがあるのでそこにリストの形で渡します。リストの形で渡しているのは複数のmiddlewareを渡すことができるためです。
Store<AppState> store = Store(reducer, initialState: AppState(0), middleware: [middleware]);
ここの時点で、僕は勘違いをしていました。結果はmiddlewareが処理された後にreduceがされるのだろうと思っていました。
実際に画面を実行すると...
何回も+ボタンを押しても何も変化しないじゃないですか!どういうこと!?ってなりました。
下記コンソールの結果(確かに出力されているが...)
Performing hot restart...
Syncing files to device iPhone XR...
Restarted application in 4,682ms.
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
flutter: 今は0です。+1するよ
middlewareを設定すると今まで直接reduceが実行されていた流れが代わり、middleware内でreduceを実行するなどして、色々とゴニョゴニョ自作しなければならないということでした。と言ってもNextDispatcherにaction名を渡すだけで済みます。
void middleware(Store<AppState> store, action, NextDispatcher next) async {
if(action == Actions.Increment) {
print('今は${store.state.counter}です。+1するよ');
}
next(action);
}
上記のように変更して実行すると...
ちゃんとインクリメントされるようになり、いい感じです。つまりnext(action)を書く場所によって
middleware処理
reduce処理
middleware処理
のような挟んだ形も実装できるわけですね。とても便利です。どのActionを取るのかもわかるし、Actionごと状態遷移(実行前後)のログを取るのもやりやすいですね!
Performing hot restart...
Syncing files to device iPhone XR...
Restarted application in 5,269ms.
flutter: 今は0です。+1するよ
flutter: 今は1です。+1するよ
flutter: 今は2です。+1するよ
nextを挟み撃ちしましょう。ついでに実行するActionも表示しちゃいます。
void middleware(Store<AppState> store, action, NextDispatcher next) async {
print('$action を実行するよ');
if(action == Actions.Increment) {
print('今は${store.state.counter}です。+1するよ');
}
next(action);
if(action == Actions.Increment) {
print('${store.state.counter}になったよ');
}
}
実行してみると...
Performing hot restart...
Syncing files to device iPhone XR...
Restarted application in 5,239ms.
flutter: Actions.Increment を実行するよ
flutter: 今は0です。+1するよ
flutter: 1になったよ
flutter: Actions.Increment を実行するよ
flutter: 今は1です。+1するよ
flutter: 2になったよ
flutter: Actions.Increment を実行するよ
flutter: 今は2です。+1するよ
flutter: 3になったよ
ということでmiddlewareの動きもなんとなくわかったところでおしまいです。
もし間違い等ありましたらご指摘をいただけると助かります
ソースコード