Redux を使って見通しのよいプログラムを書きます。

この内容は、中上級者向けに書かれています。 Redux の哲学を十分に理解した上でご覧ください。

プラグインのインストール

pubspec.yamldependencies ブロックの中に、 flutter_redux: ^0.5.0を記述します。

例えば、

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.0
  flutter_redux: ^0.5.0

のようになります。

flutter_redux0.5.0 はプラグインのバージョンです。 こちら に最新のバージョンが載っていますので確認してください。

そして、ターミナルを開き、

$ flutter packages get

とします。これでインストールは完了です。

import

まず、 .dart ファイルの一番上の方に、

import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';

と書き、 Redux を Flutter で使えるようにします。

Stateの定義

State の定義です。今回は、 count という、数値を Redux で管理してみます。初期値は 0 です。

class AppState {
  final int count;

  AppState({
    this.count,
  });

  AppState.initialState()
      : count = 0;
}

Actionの定義

Action をクラスとして定義します。 プロパティを定義しても良いでしょう。

今回は、 +1 、 -1 するような Action を定義してみます。

class HomeMinusOneAction {}

class HomePlusOneAction {}

Reducerの定義

Reducer の定義です。 +1 、 -1 して次の State を返します。

AppState appReducer(AppState state, action) {
  if (action is HomeMinusOneAction) {
    return AppState(count: state.count - 1);
  }
  if (action is HomePlusOneAction) {
    return AppState(count: state.count + 1);
  }
  return AppState(count: state.count);
}

のようになります。

実際のアプリに埋め込んで見る

いつも使っている MaterialAppStoreProvider でラップします。

class MyApp extends StatelessWidget {
  final Store<AppState> store;

  MyApp()
      : store = Store<AppState>(
      appReducer, initialState: AppState.initialState());

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        home: MyHomePage(title: 'わかりやすい'),
      ),
    );
  }
}

のようになります。

各ページの定義

各ページは、 StoreConnector でラップします。

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
        converter: (store) {
          return _ViewModel(
            count: store.state.count,
            onMinusOne: () => store.dispatch(HomeMinusOneAction()),
            onPlusOne: () => store.dispatch(HomePlusOneAction()),
          );
        },
        builder: (context, viewModel) =>
            Scaffold(...

のようになります。これで、 viewModel に対して、 Action を dispatch する関数を含めて Widget に渡すことができます。

そして、ボタンを押下したときに、 viewModel に対して定義した関数を叩いてあげれば OK です。

onPressed: () => viewModel.onPlusOne()

コードの例

例えば、コードは以下のようになります。

import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';

void main() => runApp(MyApp());

class HomeMinusOneAction {}

class HomePlusOneAction {}

class AppState {
  final int count;

  AppState({
    this.count,
  });

  AppState.initialState()
      : count = 0;
}

AppState appReducer(AppState state, action) {
  if (action is HomeMinusOneAction) {
    return AppState(count: state.count - 1);
  }
  if (action is HomePlusOneAction) {
    return AppState(count: state.count + 1);
  }
  return AppState(count: state.count);
}

class MyApp extends StatelessWidget {
  final Store<AppState> store;

  MyApp()
      : store = Store<AppState>(
      appReducer, initialState: AppState.initialState());

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        home: MyHomePage(title: 'わかりやすい'),
      ),
    );
  }
}

class _ViewModel {
  final int count;
  final VoidCallback onMinusOne;
  final VoidCallback onPlusOne;

  _ViewModel({
    this.count,
    this.onMinusOne,
    this.onPlusOne,
  });
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
        converter: (store) {
          return _ViewModel(
            count: store.state.count,
            onMinusOne: () => store.dispatch(HomeMinusOneAction()),
            onPlusOne: () => store.dispatch(HomePlusOneAction()),
          );
        },
        builder: (context, viewModel) =>
            Scaffold(
                appBar: AppBar(
                  title: Text(widget.title),
                ),
                body: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text("${viewModel.count}"),
                      Padding(
                        padding: EdgeInsets.only(top: 20.0),
                        child: RaisedButton(
                          padding: EdgeInsets.all(20.0),
                          color: Colors.lightBlue,
                          onPressed: () => viewModel.onPlusOne(),
                          child: Text('Plus One'),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(top: 20.0),
                        child: RaisedButton(
                          padding: EdgeInsets.all(20.0),
                          color: Colors.lightBlue,
                          onPressed: () => viewModel.onMinusOne(),
                          child: Text('Minus One'),
                        ),
                      ),
                    ],
                  ),
                )
            )
    );
  }
}

スクリーンショット

上のボタン「Plus One」を押すと、 +1 され、 下のボタン「Minus One」を押すと、 -1 されます。

参考リンク