Help us understand the problem. What is going on with this article?

FlutterでReduxに入門する

More than 1 year has passed since last update.

はじめに

Flutterに関わらずSPAやモバイル開発をすると、アプリケーションの状態管理をどうしようかと悩む時があります。もし状態管理がないまま開発をすると、各ページに状態を引き継ぐ処理が必要になります。(とっても面倒臭い..!)
そんな状態をアプリケーションの中で集中管理できれば、状態を引き継ぐ処理などが減り、開発が効率的に行えます。

Reduxについて

状態管理をするためのアーキテクチャーとして、Reduxがあります。reduxはよくreactと一緒に使われることが多いんですが、Flutterでもreduxが使えます。

主な登場人物は
Action
Reducer
Store
Middleware(使うかどうか自由)
です。

色々調べているとこんな図をよく見かけました。データフローは単方向なんですね。

basic@2x.png

middleware@2x.png

Actionは処理名の定義です。
実際の処理はReducerが担います。Reducerは前の状態を受け取り新しいものを作るという副作用のない関数になります。(例:Aを入れたら毎回Bが返る)
Storeがアプリケーション全体の状態(state)を保持している役割をします。Store内の状態はReducer経由でないと変更できません。

では副作用があるものはどうしたらいいのでしょうか。例えばアプリケーションで必ず発生する外部APIは副作用のあるものとされます。そのような外部APIとの連携はMiddlewareと呼ばれる部分が担当します。MiddlewareはReducerの前後に置けます。

と色々なサイトで見たきた内容を簡単にまとめましたが、理解するにはやはりコードを書いた方が早いのでコードを書いてみます。

実際に実装してみると下図のようイメージになります。Storeを通して色々とやりとりすることになります。

Middlewareを使わないパターン
redux@2x.png

Middlewareを利用するパターン
redux_with_md@2x.png

Flutterで実装

ここではカウントアップを例に実装してみます。

Flutterプロジェクトの作成

Flutterプロジェクトを作成します。

$ flutter create learn_redux

今回はとても簡単なプログラムなので、全てmain.dartに実装していきます。本当はフォルダ分けして管理する方がよいと思います。

Reduxパッケージの導入

pubからReduxパッケージを落としてきます。
各バージョンはpubでそれぞれのページから確認ください。
redux
flutter_redux

pubspec.yaml
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という名前がデファクトのようです。

lib/main.dart
import 'package:meta/meta.dart';

@immutable
class AppState {
  final int counter;
  AppState(this.counter);
}

ここではcounterというプロパティを持ったクラスを定義します。コンストラクタに整数を受け取る形です。このような形で状態管理したい項目をプロパティに持つような形をとります。

Actionの定義

次にActionの定義です。ActionはReducerを実行する処理名を定義するような位置付けと冒頭に書きました。classやenumの形どちらでもよいですが、今回はenumで作りました。先ほどのAppStateクラスの下あたりに書きます。

lib/main.dart
enum Actions { Increment }

Reducerの定義

Reducerは実際に処理をするところでした。その処理というのはActionで定義されている処理しか行いません。

reducerは引数が二つあります。1つ目が処理する前の状態AppStateのインスタンスです。第2引数がActionです。

今回はActions.IncrementというActionの時しか処理しないためif文処理を書きます。返り値は新しいAppStateインスタンスです。

冒頭でReducerは副作用のない関数と話しましたが、古い状態を入れると新しい状態が返ってくるという副作用のない関数になっていることがわかります。(古い状態を直接書き換えることはしない!)

定義しているAction以外のものは前の状態そのままを返すようにします。

lib/main.dart
AppState reducer(AppState prev, action) {
  if (action == Actions.Increment) {
    return AppState(prev.counter + 1);
  }
  return prev;
}

main.dartを完成させる

画面側を一気に書きます。

lib/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の返値はWigdetなので、ここではいつも通り好きなUIを作って挙げたらOKです!

Storeの中身を使いたい時はStoreConnectorの中に実装するということを覚えておけば良さそうですね。

ビルドしてみましょう!

learnredux.gif

おおーちゃんと動いている!!(感動)

これでReduxを使った基本的なデータの流れがわかりました。あとはmiddlewareとかいうやつです。

middlewareの定義

middlewareは第一引数にStore、第二引数にAction、第三引数にNextDispatcherという形の関数を自作します。

lib/main.dart
void middleware(Store<AppState> store, action, NextDispatcher next) async {
  if(action == Actions.Increment) {
    print('今は${store.state.counter}です。+1するよ');
  }
}

試しにこれでやってみましょう。middleware関数を追加したので、Storeに登録する必要があります。Storeを作成するところで引数でmiddlewareがあるのでそこにリストの形で渡します。リストの形で渡しているのは複数のmiddlewareを渡すことができるためです。

lib/main.dart
  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名を渡すだけで済みます。

lib/main.dart
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も表示しちゃいます。

lib/main.dart
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の動きもなんとなくわかったところでおしまいです。
もし間違い等ありましたらご指摘をいただけると助かります:pray:

ソースコード

https://github.com/yujikawa/learn_redux

yujikawa
Pythonが好きです。データ分析基盤周りのお仕事をしてます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした