flutter_reduxのREADMEのexampleをもうちょっとimmutableにしながら学習する


概要

flutter_reduxを使ってFlutterにReduxを導入します。

https://pub.dev/packages/flutter_redux のREADMEをベースに解説するだけ…のつもりだったのですが、exampleがimmutableな書き方でなかったのでちょくちょく修正しながらやってみる記事です。

成果物はincrementするカウンターです。

この記事で説明しないこと

実際にやってみた

雑にセクションごとに解説していきます。

新規プロジェクト作成とインストール

flutter_redux $ flutter create .

pubspec.yamlにflutter_reduxを追加します。

dependencies:
  flutter_redux: ^0.6.0

Stateの定義

react reduxにおいてはStateはplain old javascript objects (pojo)でした。

しかし、flutter_reduxののサンプルではstateがただのintとして名前も付けられずに定義されており、これではスケールしないのでとりあえずクラスで定義します(@immutableも付けます)。

@immutable
class State {
  State({this.counter = 0});
  int counter;

  State copyWith({counter}) {
    return State(
      counter: counter ?? this.counter,
    );
  }
}

copyWithメソッドが生えていますが一旦気にしないでください。Reducerの項で解説します。

Actionの定義

何のことはない。enumで定義します。

enum Actions { Increment }

Reducerの定義

Reducerは(oldState, action) => newStateな純粋関数なのでそのまま実装します。

State counterReducer(State state, Object action) {
  if (action == Actions.Increment) {
    state.counter++;
  }

  return state;
}

…と言いたいところなのですが、サンプルではこのようにstateのpropertyを直接編集していたりifで書いてたりします。
immutableでない点とケースが完全に網羅されているか分からない点に違和感があるので以下のように書き直します。

State counterReducer(State state, Object action) {
  switch (action) {
    case Actions.Increment:
      return state.copyWith(counter: state.count + 1);
      break;
    default:
      return state;
  }
}

Stateに生やしたcopyWith()はここで使います。
stateのプロパティを編集して新しいstate(かのように)返す関数は純粋関数とは呼べないため、copyWithを使って書き換えたい項目だけ更新して新しいインスタンスを返させるようにしました。

ここまではただ単にreduxの世界の話です。ここまででuni-directionalなデータフローは完成しました。

(とりあえずimmutableっぽくなるように実装したのですが、exampleがこのような書き方になってる理由をご存じの方がいらっしゃったらご教授ください…🙏 ざっと調べた感じ実際にflutterでreduxを実装する時はこのように実装する人が多いように見えております…)

UIを実装する

ここから実際にstateを表示するUIと、ActionをdispatchするUIを実装します。

storeを定義する

storeはstateを持ち、stateを変更する唯一の方法であるactiondispatchする関数を持ちます。
要するにstateとactionを結びつける役割を持ちます。

参考:

A store holds the whole state tree of your application. The only way to change the state inside it is to dispatch an action on it.
https://redux.js.org/api/store#store

storeはアプリケーションのあらゆる箇所で必要とされるため、main()の直下で定義します。



void main() {
  final store = Store<State>(counterReducer, initialState: State(count: 0));
  runApp(
    StoreProvider<State>(
      store: store,
      child: MyApp(title: 'Flutter Redux Demo'),
    ),
  );
}

カウンターとボタンの実装

下位Widgetへstoreを渡す準備ができたので実際にUIを作っていきます。

カウンター部分を表示するUIはstateの変更を監視したいため、都度UIをrebuildするStoreConnector()を、クリックする度にカウンターをincrementするボタンのUIは、actionをdispatchする機能を持たせますがstateの変更をリッスンする必要がないためStoreProvider.of()を使います。

class MyApp extends StatelessWidget {
  MyApp({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: new ThemeData.dark(),
      title: title,
      home: Scaffold(
        appBar: AppBar(title: Text(title)),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text('you have pushed the button this many times'),
                StoreConnector<State, int>(
                  converter: (store) => store.state.count,
                  builder: (context, count) {
                    return Text(
                      count.toString(),
                      style: Theme.of(context).textTheme.display1,
                    );
                  },
                ),
              ]),
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () =>
              StoreProvider.of<State>(context).dispatch(Actions.Increment),
        ),
      ),
    );
  }
}

StoreConnectorについて

StoreConnectorはstoreの変更を検知してwidgetをrebuildしますが、微妙に使い方が分からなかったので補足します。

  1. StoreConnector<S, ViewModel>は1つ目のジェネリック型にはstateの型を、2つ目にはconverterの返り値でありbuilderの第2引数になるViewModelの型を書きます。ViewModelと言うとピンとこないですが、要するに最終的にbuilderで使いたいようにstoreを加工してやった後の型を書けば良いです。今回で言うとカウンターの数字があればよいのでintになります。
  2. converterにはの第1引数にstoreを受けてViewModelを返す関数を書きます。
  3. builderにはBuildContext,ViewModelを引数にウィジェットを返すbuilder関数を書きます。

StoreProvider.ofについて(actionのdispatch)

StoreProvider.of<State>(context)でstoreが取れるので、これに生えたdispatchの引数にActionを渡せばreducerがstateを更新してくれます。

補足: StoreProviderは変更をリッスンしないと思っているのですが、間違っていればご指摘ください…🙇
https://pub.dev/documentation/flutter_redux/latest/flutter_redux/StoreProvider-class.html

まとめ

出来ました。

flutter_redux自体、dartの性質も相まってライブラリとしてはそこまでimmutablityを推していないのかな…?と感じました。
実際、公式のtodo exampleを除くと自前でcopyWithを実装していたりするのでこれがスタンダードなのかな…?と思いました。

次は非同期処理かもう少し大きな規模のアプリを作りたいと思います。