【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる③

いよいよ本シリーズの最終篇になります。
本篇は、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理のreduxパターンを説明します。

##三、reduxでテーマ切替&多言語化
>flutter_redux: ^0.6.0

Blocと同じのようにreduxにも３つのポイントがある。それは **状態State**、**アクションAction**、**変化させる者Reducer**。

>会社制に例えると、社長が、仕事を各部門に分与し、部門長ははまたそれぞれ部内の業務を部員に委託する。各々が責任を取って行動する。reduxにおいて、全ての状態はstoreによって集中管理し、社長AppStateから階層的に分配していく。

<img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.52.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/de3906b6-52d1-c8e9-3da0-1016f74bc22a.png"> <img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.52.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/7253fe4f-61d0-c873-f2a9-de057036ca81.png"> <img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.53.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/4b9c8893-38b9-9bda-2685-897df9aeaee8.png"> <img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.53.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/cdda45d4-6226-89ef-0f98-d30ef221e302.png"> <img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.53.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/6e9fea31-5e3a-c482-4c64-fcff17f92510.png"> <img width="255" alt="Simulator Screen Shot - iPhone 11 Pro - 2020-08-02 at 23.53.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/a0ee58d1-ef88-6a57-924a-e4584f80c25f.png">

###1.redux状態、アクション、変化させる者の準備
>社長

```dart:appState_redux.dart
import 'themeState_redux.dart';
import 'localState_redux.dart';

class AppState {
  final ReduxThemeState themeState;//テーマ状態
  final ReduxLocaleState localeState;//言語状態
  AppState({this.themeState, this.localeState});

  factory AppState.initial()=> AppState(
      themeState: ReduxThemeState.initial(),
      localeState: ReduxLocaleState.initial()
  );
}

//集中的に状態を分配
AppState appReducer(AppState prev, dynamic action)=>
    AppState(
      themeState:themeDataReducer(prev.themeState, action),
      localeState: localReducer(prev.localeState, action),);

```
>部門長① theme

```dart:themeState_redux.dart
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';

//テーマ状態の切り替え
class ReduxThemeState extends ChangeNotifier {
  ThemeData themeData; 
  int colorIndex; 
  
  ReduxThemeState(this.themeData,this.colorIndex);

  factory ReduxThemeState.initial()=> ReduxThemeState(ThemeData(primaryColor: Colors.green),1);
}

//テーマアクションの切り替え
class ActionSwitchTheme {
  final ThemeData themeData;
  final int colorIndex;
  ActionSwitchTheme(this.themeData,this.colorIndex);
}

//変化させる者の切り替え
var themeDataReducer = TypedReducer<ReduxThemeState, ActionSwitchTheme>((state, action) =>
    ReduxThemeState(action.themeData,action.colorIndex));

```
>部門長② local

```dart:localState_redux.dart
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';

//言語状態の切り替え
class ReduxLocaleState{
  Locale locale;
  ReduxLocaleState(this.locale);
  factory ReduxLocaleState. initial()=> ReduxLocaleState(Locale('ja', 'JP'));
}

//言語アクションの切り替え
class ActionSwitchLocal {
  final Locale locale;
  ActionSwitchLocal(this.locale);
  factory ActionSwitchLocal.ja()=> ActionSwitchLocal(Locale('ja', 'JP'));
  factory ActionSwitchLocal.en()=> ActionSwitchLocal(Locale('en', 'US'));
}

//変化させる者の切り替え
var localReducer = TypedReducer<ReduxLocaleState, ActionSwitchLocal>((state,  action) => 
    ReduxLocaleState(action.locale,));
```



###2.reduxプロパティーの使用

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; //providerパターンのpubspec.yaml配置を参照
import 'home_page.dart';
import 'provider/I18nDelegate.dart'; //providerパターン紹介時のファイル使用
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'redux/appState_redux.dart';

void main() {
  runApp(Wrapper(child: MyApp()));
}

class Wrapper extends StatelessWidget {
  final Widget child;
  Wrapper({this.child});

  @override
  Widget build(BuildContext context) {
    //StoreProviderによってラップする、storeプロパティーでAppStateを配置
    return StoreProvider(
        store: Store<AppState>(appReducer, initialState: AppState.initial()),
        child: MyApp());
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreBuilder<AppState>(builder: (context,store) => MaterialApp(
                  title: "状態管理Demo",
                  localizationsDelegates: [
                    GlobalMaterialLocalizations.delegate,
                    GlobalWidgetsLocalizations.delegate,
                    I18nDelegate.delegate //言語デリゲート シリーズ初編のproviderパターンのファイルを利用
                  ],
                  locale: store.state.localeState.locale,
                  supportedLocales: [store.state.localeState.locale],
                  debugShowCheckedModeBanner: false,
                  home: HomePage(),
                ));
  }
}

```
>状態の切り替えは **store.dispatch(~action)** を使う

```dart:home_page.dart
import 'package:flutter/material.dart';
import 'provider/i18n.dart'; //providerパターン紹介時のファイル使用
import 'package:flutter_redux/flutter_redux.dart';
import 'redux/appState_redux.dart';
import 'redux/themeState_redux.dart';
import 'redux/localState_redux.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    var screenWidth = MediaQuery.of(context).size.width;
    var statusBarH = MediaQuery.of(context).padding.top;
    var naviBarH = kToolbarHeight;

    //StoreBuilder<AppState>(builder: (context,store) を通じて状態ゲット
    return StoreBuilder<AppState>(
      builder: (context, store) => Scaffold(
        appBar: AppBar(
          backgroundColor: store.state.themeState.themeData.primaryColor,
          title: Text(
            I18N.of(context).title,
            style: TextStyle(color: Colors.white),
          ),
        ),
        drawer: Drawer(
          child: Column(
            children: <Widget>[
              Container(
                width: screenWidth,
                height: statusBarH + naviBarH,
                color: store.state.themeState.themeData.primaryColor,
              ),
              SizedBox(height: 10.0),
              Row(
                children: <Widget>[
                  Padding(padding: EdgeInsets.all(10)),
                  Wrap(
                    children: <Widget>[
                      RaisedButton(
                        color: Colors.green,
                        onPressed: () {
                          //イベントトリガー、重要
                          store.dispatch(ActionSwitchTheme(
                              ThemeData(primaryColor: Colors.green), 1));
                        },
                        child: Text(
                          I18N.of(context).greenBtn,
                          style: TextStyle(color: Colors.white),
                        ),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.red,
                        onPressed: () {
                          store.dispatch(ActionSwitchTheme(
                              ThemeData(primaryColor: Colors.red), 2));
                        },
                        child: Text(
                          I18N.of(context).redBtn,
                          style: TextStyle(color: Colors.white),
                        ),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.blue,
                        onPressed: () {
                          store.dispatch(ActionSwitchTheme(
                              ThemeData(primaryColor: Colors.blue), 3));
                        },
                        child: Text(
                          I18N.of(context).blueBtn,
                          style: TextStyle(color: Colors.white),
                        ),
                        shape: CircleBorder(),
                      ),
                    ],
                  ),
                ],
              ),
              Divider(),
              SizedBox(
                height: 10,
              ),
              Row(
                children: <Widget>[
                  Padding(padding: EdgeInsets.all(10)),
                  SizedBox(
                    width: 15,
                  ),
                  RaisedButton(
                    color: store.state.themeState.themeData.primaryColor,
                    onPressed: () {
                      store.dispatch(ActionSwitchLocal.ja());
                    },
                    child: Text(
                      "日本語",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                  SizedBox(
                    width: 15.0,
                  ),
                  RaisedButton(
                    color: store.state.themeState.themeData.primaryColor,
                    onPressed: () {
                      store.dispatch(ActionSwitchLocal.en());
                    },
                    child: Text(
                      "English",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
        body: ListView(
          children: <Widget>[
            Center(
              child: Column(
                children: <Widget>[
                  SizedBox(height: 10.0),
                  Container(
                    width: 200,
                    height: 300,
                    color: store.state.themeState.themeData.primaryColor,
                  ),
                  SizedBox(height: 10.0),
                  Text(
                    I18N.of(context).analects,
                    style: TextStyle(
                        color: store.state.themeState.themeData.primaryColor,
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 30.0),
                  FloatingActionButton(
                      onPressed: () {},
                      backgroundColor:
                          store.state.themeState.themeData.primaryColor,
                      child: Icon(Icons.check)),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }
}


```

##まとめ
いかがでしたか。本篇をもってflutterの状態管理シリーズを完了とします。総じて言えば、3パターンともに構造的に、実現方式的に似ているようなところがあります。**provider**はflutter公式が推奨するパターンとして使っても無難でしょう。Streamに対する理解を深めれば、**Bloc**の使用は少し上級感が感じられます。**redux**については構造は明白で、このパターンを取る方も少なくないと思います。
どのパターンを採用するのかは正解がなく、プロジェクトのニーズに合わせて決めておけば良いでしょう。また、状態を変更する必要のあるwidgetがそれほど無ければ、基礎のsetState()というapiを使っては分かりやすいでしょう。