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

Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。

本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。


##一、providerでテーマ切替&多言語化
>provider: ^4.3.1 　(providerパッケージをpub.devから取得)

###1.テーマ切替
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
<img width="255" alt="color1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/5d5fd96f-7e1d-f530-ab1e-f7cbfac53643.png"><img width="255" alt="color2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/ca2095d0-33a0-1758-11bb-7bcf98f72622.png"><img width="255" alt="color3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/b1fbb895-8d1c-e31f-4bce-eb39abf4a996.png"><img width="255" alt="color4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/0b047fcb-219b-faf2-24e3-bc80c06d6498.png"><img width="255" alt="color5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/ea95a52a-454a-b494-83df-7b03c9cd598a.png"><img width="255" alt="color6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/4027ebad-b82d-84ce-8eba-22a6ffdfe08c.png">


####1.1.状態クラス
>_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。
>>changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。

```dart:theme_state.dart
import 'package:flutter/material.dart';

class ProviderThemeState extends ChangeNotifier{
  ThemeData _themeData;//テーマ
  int _colorIndex;//色ボタン選択中の状態を記録

  ProviderThemeState(this._themeData, this._colorIndex);

  void changeThemeData(ThemeData themeData, int colorIndex){

    this._themeData = themeData;
    this._colorIndex = colorIndex;
    notifyListeners();

  }

  int get colorIndex => this._colorIndex;
  ThemeData get themeData => this._themeData;
}

```

####1.2.管理する必要がある部分をラップする(wrap)
>MultiProviderは複数のproviderを入れることができる。widgetをwrapperだけでなく他のコンポーネントでもラップやカバーできる。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';

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

class Wrapper extends StatelessWidget{
  
  final Widget child;
  Wrapper({this.child});
  
  @override
  Widget build(BuildContext context) {
    
    final initThemeData = ThemeData(
      primaryColor: Colors.green,
    );
    
    final initIndex = 1;
    
    return MultiProvider(providers: [
      ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),
    ],child: child,);

  }
  
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderThemeState>(builder: (context,state,widget) =>MaterialApp(
      title: "状態管理Demo",
      theme: state.themeData,
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ));
  }
}

```

####1.3.状態の使用、メソッドの実行
>Provider.of(context)よりConsumerのほうは結構細かい対象を絞れるため、widget再ビルドの消費コストが削減される。また、ConsumerはwidgetのBuildContextが不要で使えるのも一大メリット。
>>ボタンの押下により、changeThemeData()が呼び出されConsumerの状態の状態が更新される。

```dart:home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/theme_state.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;

    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter状態管理",style: TextStyle(color: Colors.white),),
      ),
      drawer: Drawer(
        child: Consumer<ProviderThemeState>(builder: (context, state, widget) {
          return Column(
            children: <Widget>[
              Container(
                width: screenWidth,
                height: statusBarH + naviBarH,
                color: state.themeData.primaryColor,
              ),
              SizedBox(height: 10.0),
                  Row(
                    children: <Widget>[
                      Padding(padding: EdgeInsets.all(10)),
                      Wrap(children: <Widget>[
                        RaisedButton(
                          color: Colors.green,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.green), 1);

                          },
                          child: Text("緑",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                        RaisedButton(
                          color: Colors.red,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
                          },
                          child: Text("赤",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                        RaisedButton(
                          color: Colors.blue,
                          onPressed: () {
                            state.changeThemeData(ThemeData(primaryColor: Colors.blue), 3);
                          },
                          child: Text("青",style: TextStyle(color: Colors.white),),
                          shape: CircleBorder(),
                        ),
                      ],),

                    ],
                  ),
              Divider(),
              SizedBox(height: 10,),
              Row(children: <Widget>[
                Padding(padding: EdgeInsets.all(10)),
                SizedBox(width: 15,),
                RaisedButton(
                  color: state.themeData.primaryColor,
                  onPressed: () {

                  },
                  child: Text("日本語",style: TextStyle(color: Colors.white),),
                ),
                SizedBox(width: 15.0,),
                RaisedButton(
                  color: state.themeData.primaryColor,
                  onPressed: () {

                  },
                  child: Text("English",style: TextStyle(color: Colors.white),),
                ),
              ],),

                ],
          );
        }),
      ),
      body: ListView(
        children: <Widget>[
          Consumer<ProviderThemeState>(builder: (context, state, widget) {
            return Center(
              child: Column(
                children: <Widget>[
                  SizedBox(height: 10.0),
                  Container(
                    width: 200,
                    height: 300,
                    color: state.themeData.primaryColor,
                  ),
                  SizedBox(height: 10.0),
                  Text(
                    "学びて思わざればすなわち罔（くら）し、\n思いて学ばざればすなわち殆（あやう）し",
                    style: TextStyle(
                        color: state.themeData.primaryColor,
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 30.0),
                  FloatingActionButton(onPressed: (){

                  },
                  backgroundColor: state.themeData.primaryColor,
                  child: Icon(Icons.check)),
                ],
              ),
            );
          })
        ],
      ),
    );
  }

}

```


###2.言語切替
>flutter_localizations:
sdk: flutter　（多言語をpubspec.yamlで配置）

ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。

<img width="255" alt="language1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/a85757b2-d06f-3a05-e0c4-6e53a9766130.png"><img width="255" alt="language2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/739a9a5f-e87c-1464-1804-61e91e6f2349.png"><img width="255" alt="language3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/cda1b8e0-97d7-8abf-5094-0b7edaf775cb.png"><img width="255" alt="language4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/fc0be238-491e-8227-7018-3dee7b65131d.png">





####2.1.言語データを準備する

```dart:language_data.dart
class LanguageData {

  static final EN = {
    "title":"Flutter State Management",
    "greenBtn":"Green",
    "redBtn":"Red",
    "blueBtn":"Blue",
    "analects":"To learn without thinking is blindness, \nto think without learning is idleness (Confucius)."
  };

  static final JP = {
    "title":"Flutter状態管理",
    "greenBtn":"緑",
    "redBtn":"赤",
    "blueBtn":"青",
    "analects":"学びて思わざればすなわち罔（くら）し、\n思いて学ばざればすなわち殆（あやう）し"
  };

}

```

####2.2.言語データを使用するためのクラス


```dart:i18n.dart
import 'package:flutter/material.dart';
import 'language_data.dart';

class I18N {

  final Locale locale;
  I18N(this.locale);
  static Map<String, Map<String, String>> _localizedValues = {
    "en": LanguageData.EN,
    "ja": LanguageData.JP,
  };

  static I18N of(BuildContext context) {
    return Localizations.of(context, I18N);
  }

  get title {
    return _localizedValues[locale.languageCode]['title'];
  }

  get greenBtn {
    return _localizedValues[locale.languageCode]['greenBtn'];
  }

  get redBtn {
    return _localizedValues[locale.languageCode]['redBtn'];
  }

  get blueBtn {
    return _localizedValues[locale.languageCode]['blueBtn'];
  }

  get analects {
    return _localizedValues[locale.languageCode]['analects'];
  }

}

```

####2.3.多言語のデリゲートクラス

```dart:I18nDelegate.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'i18n.dart';

class I18nDelegate extends LocalizationsDelegate<I18N> {
  @override
  bool isSupported(Locale locale) {
    ///サポートする言語
    return ['en', 'ja'].contains(locale.languageCode);
  }

  ///現在言語環境下の文字列をロード
  @override
  Future<I18N> load(Locale locale) {
    return SynchronousFuture<I18N>(I18N(locale));
  }

  @override
  bool shouldReload(LocalizationsDelegate<I18N> old) {
    return false;
  }

  //グローバル静的デリゲート
  static I18nDelegate delegate = I18nDelegate();

}

```


####2.4.状態クラス
>localeというフィールドしか使わない

```dart:locale_state.dart
import 'package:flutter/material.dart';

class LocaleState extends ChangeNotifier{
  Locale _locale;//地域
  LocaleState(this._locale);

  factory LocaleState.jp()=>
      LocaleState(Locale('ja', 'JP'));

  factory LocaleState.en()=>
      LocaleState(Locale('en', 'US'));

  void changeLocaleState(LocaleState state){
    _locale=state.locale;
    notifyListeners();
  }

  Locale get locale => _locale; //言語ゲット
}

```

####2.5.状態の使用、メソッドの実行
>Consumer() の他に **Consumer2()** から Consumer6() まであり、得たい状態値の種類数によって使い分けることができる。

#####2.5.1.多言語化の配置

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';
import 'provider/locale_state.dart';
import 'provider/I18nDelegate.dart';

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

class Wrapper extends StatelessWidget{
  
  final Widget child;
  Wrapper({this.child});
  
  @override
  Widget build(BuildContext context) {
    
    final initThemeData = ThemeData(
      primaryColor: Colors.green,
    );
    
    final initIndex = 1;
    
    return MultiProvider(providers: [
      ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),//themeのprovider
      ChangeNotifierProvider(create: (_) => LocaleState.jp()),//localeのprovider
    ],child: child,);

  }
  
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>MaterialApp(
      title: "状態管理Demo",
      localizationsDelegates: [GlobalMaterialLocalizations.delegate,
      GlobalWidgetsLocalizations.delegate,
        I18nDelegate.delegate//言語デリゲート
      ],
      locale: localState.locale,
      supportedLocales: [localState.locale],
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    ));
  }
}

```

#####2.5.2.多言語化の実行

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/locale_state.dart';
import 'provider/theme_state.dart';
import 'provider/i18n.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;

    return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>Scaffold(
        appBar: AppBar(
          backgroundColor: themeState.themeData.primaryColor,
          title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),),
        ),
        drawer: Drawer(
          child: Consumer2<ProviderThemeState,LocaleState>(builder: (_,themeState,localState,__) {
            return Column(
              children: <Widget>[
                Container(
                  width: screenWidth,
                  height: statusBarH + naviBarH,
                  color: themeState.themeData.primaryColor,
                ),
                SizedBox(height: 10.0),
                Row(
                  children: <Widget>[
                    Padding(padding: EdgeInsets.all(10)),
                    Wrap(children: <Widget>[
                      RaisedButton(
                        color: Colors.green,
                        onPressed: () {
                          themeState.changeThemeData(ThemeData(primaryColor: Colors.green), 1);

                        },
                        child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.red,
                        onPressed: () {
                          themeState.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
                        },
                        child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.blue,
                        onPressed: () {
                          themeState.changeThemeData(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: themeState.themeData.primaryColor,
                    onPressed: () {
                      localState.changeLocaleState(LocaleState.jp());
                    },
                    child: Text("日本語",style: TextStyle(color: Colors.white),),
                  ),
                  SizedBox(width: 15.0,),
                  RaisedButton(
                    color: themeState.themeData.primaryColor,
                    onPressed: () {
                      localState.changeLocaleState(LocaleState.en());
                    },
                    child: Text("English",style: TextStyle(color: Colors.white),),
                  ),
                ],),

              ],
            );
          }),
        ),
        body: ListView(
          children: <Widget>[
            Consumer2<ProviderThemeState,LocaleState>(builder: (_, themeState, localeState,__) {
              return Center(
                child: Column(
                  children: <Widget>[
                    SizedBox(height: 10.0),
                    Container(
                      width: 200,
                      height: 300,
                      color: themeState.themeData.primaryColor,
                    ),
                    SizedBox(height: 10.0),
                    Text(I18N.of(context).analects,
                      style: TextStyle(
                          color: themeState.themeData.primaryColor,
                          fontSize: 18.0,
                          fontWeight: FontWeight.bold),
                    ),
                    SizedBox(height: 30.0),
                    FloatingActionButton(onPressed: (){

                    },
                        backgroundColor: themeState.themeData.primaryColor,
                        child: Icon(Icons.check)),
                  ],
                ),
              );
            })
          ],
        ),
      ),

    );
  }

}


```

##まとめ
本文ではProviderでテーマや多言語の切替という多状態を管理してみました。Flutterの状態管理はandroid, iOSと違い、多分多くの方が悩み続けていると思いますね。Flutterの状態管理について、代表的にprovider,BLoC,reduxがありますので、次回ではBLoCパターンをご紹介していきたいと思います。