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

お待たせしました。プロジェクトの人員異動でバタバタしていました。
さて、前回ではproviderという実現方式でflutterの状態管理を説明しました。今回はBLoC(Business Logic Component)を利用し、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理を説明します。

##二、BLoCでテーマ切替&多言語化
Blocには３つのポイントがある。それは **業務ロジックユニット**、**状態**、**イベント**。
>flutter_bloc: ^6.0.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="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"> <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">



###1.テーマ切替のBloc
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。


####1.1状態クラス
>テーマ関連の状態量を抽象クラスに置きながら、他の状態はこれを継承し状態の細分化を実現する。

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

@immutable
abstract class BlocThemeState {
  final ThemeData themeData;//テーマ
  final int colorIndex;//色ボタン選択中の状態を記録

  BlocThemeState(this.themeData, this.colorIndex);
}

class InitBlocThemeState extends BlocThemeState {
  //初期化テーマは緑にする
  InitBlocThemeState(ThemeData themeData, int colorIndex) : super(ThemeData(primaryColor: Colors.green), 1);
}

class BlocStateImpl extends BlocThemeState {
  BlocStateImpl(ThemeData themeData, int colorIndex) : super(themeData, colorIndex);
}

```

####1.2イベントクラス
>イベントをここで定義する。パラメーターの切り替え、状態のリセットなど。

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

@immutable
abstract class BlocThemeEvent {}

class BlocEventSwitchTheme extends BlocThemeEvent{
  final ThemeData themeData;
  final int colorIndex;
  BlocEventSwitchTheme(this.themeData,this.colorIndex);
}

class BlocEventResetTheme extends BlocThemeEvent{}

```

####1.3業務ロジッククラス
>このクラスはBlocの核と言っても過言ではない。イベントを通じて状態を生成する。

```dart:theme_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:state_dancer/BLoC/theme_event.dart';
import 'package:state_dancer/BLoC/theme_state.dart';

class ThemeBloc extends Bloc<BlocThemeEvent,BlocThemeState> {

  //overrideメソッド、状態の初期化
  @override
  ThemeBloc(BlocThemeState initialState) : super(initialState);

  @override
  //async* yield 非同期処理関数を生成(あまり使わない)　一々setState()書かなくても済むのはメリット
  Stream<BlocThemeState> mapEventToState(BlocThemeEvent event) async* {
    //テーマ切替
    if(event is BlocEventSwitchTheme) {
      yield BlocStateImpl(event.themeData, event.colorIndex);
    }

    //テーマリセット
    if(event is BlocEventResetTheme) {
      yield InitBlocThemeState();
    }
  }

}

```

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

####2.1状態クラス

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

@immutable
abstract class BlocLocaleState {
  final Locale locale;
  BlocLocaleState(this.locale);
}

class JpBlocLocaleState extends BlocLocaleState {
  JpBlocLocaleState() : super(Locale('ja', 'JP'));
}

class InitBlocLocaleState extends JpBlocLocaleState {}

class EnBlocLocaleState extends BlocLocaleState {
  EnBlocLocaleState() : super(Locale('en', 'US'));
}

```

####2.2イベントクラス

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

@immutable
abstract class BlocLocaleEvent {}

class BlocEventSwitch2JP extends BlocLocaleEvent{}

class BlocEventSwitch2EN extends BlocLocaleEvent{}

```

####2.3業務ロジッククラス

```dart:lang_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:state_dancer/BLoC/lang_event.dart';
import 'package:state_dancer/BLoC/lang_state.dart';

class LocalBloc extends Bloc<BlocLocaleEvent,BlocLocaleState> {
  //初期化コンストラクター
  @override
  LocalBloc(BlocLocaleState initialState) : super(initialState);

  @override
  Stream<BlocLocaleState> mapEventToState(BlocLocaleEvent event) async* {
    if(event is BlocEventSwitch2JP) {
      yield JpBlocLocaleState();
    }

    if(event is BlocEventSwitch2EN) {
      yield EnBlocLocaleState();
    }
  }

}

```

###3.Blocの使用
>providerパターンと同様に、管理する必要のある状態をMultiBlocProviderの中に入れる。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';//providerパターンのpubspec.yaml配置を参照
import 'package:state_dancer/BLoC/lang_state.dart';
import 'package:state_dancer/BLoC/theme_state.dart';
import 'home_page.dart';
import 'provider/I18nDelegate.dart';//providerパターン紹介時のファイルを使用
import 'BLoC/theme_bloc.dart';
import 'BLoC/lang_bloc.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(providers: [
      BlocProvider<ThemeBloc>(create: (context) => ThemeBloc(InitBlocThemeState()),),
      BlocProvider<LocalBloc>(create: (context) => LocalBloc(InitBlocLocaleState()),),
    ], child: MyApp());
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeBloc, BlocThemeState>(
        builder: (_, theme) => BlocBuilder<LocalBloc, BlocLocaleState>(
            builder: (_, local) => MaterialApp(
                  title: "状態管理Demo",
                  localizationsDelegates: [
                    GlobalMaterialLocalizations.delegate,
                    GlobalWidgetsLocalizations.delegate,
                    I18nDelegate.delegate //言語デリゲート 前回providerパターンのファイルを利用
                  ],
                  locale: local.locale,
                  supportedLocales: [local.locale],
                  debugShowCheckedModeBanner: false,
                  home: HomePage(),
                )));
  }
}


```

>BlocBuilder<~Bloc, ~State>(builder: (_,~State) を通じて状態ゲット;
 BlocProvider.of<~Bloc>(context).add(~Event())は状態変更のトリガー

```dart:home_page.dart
import 'package:flutter/material.dart';
import 'package:state_dancer/BLoC/lang_bloc.dart';
import 'BLoC/theme_bloc.dart';
import 'BLoC/theme_state.dart';
import 'provider/i18n.dart';//providerパターン紹介時のファイルを使用
import 'package:flutter_bloc/flutter_bloc.dart';
import 'BLoC/theme_event.dart';
import 'BLoC/lang_event.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;

    //BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) を通じて状態ゲット
    return BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) =>Scaffold(
        appBar: AppBar(
          backgroundColor: blocState.themeData.primaryColor,
          title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),),
        ),
        drawer: Drawer(
          child: BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) {
            return Column(
              children: <Widget>[
                Container(
                  width: screenWidth,
                  height: statusBarH + naviBarH,
                  color: blocState.themeData.primaryColor,
                ),
                SizedBox(height: 10.0),
                Row(
                  children: <Widget>[
                    Padding(padding: EdgeInsets.all(10)),
                    Wrap(children: <Widget>[
                      RaisedButton(
                        color: Colors.green,
                        onPressed: () {
                          //イベントトリガー、重要
                          BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.green), 1));
                        },
                        child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.red,
                        onPressed: () {
                          BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.red), 2));
                        },
                        child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),),
                        shape: CircleBorder(),
                      ),
                      RaisedButton(
                        color: Colors.blue,
                        onPressed: () {
                          BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(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: blocState.themeData.primaryColor,
                    onPressed: () {
                      BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2JP());
                    },
                    child: Text("日本語",style: TextStyle(color: Colors.white),),
                  ),
                  SizedBox(width: 15.0,),
                  RaisedButton(
                    color: blocState.themeData.primaryColor,
                    onPressed: () {
                      BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2EN());
                    },
                    child: Text("English",style: TextStyle(color: Colors.white),),
                  ),
                ],),
              ],
            );
          }),
        ),
        body: ListView(
          children: <Widget>[
            BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) {
              return Center(
                child: Column(
                  children: <Widget>[
                    SizedBox(height: 10.0),
                    Container(
                      width: 200,
                      height: 300,
                      color: blocState.themeData.primaryColor,
                    ),
                    SizedBox(height: 10.0),
                    Text(I18N.of(context).analects,
                      style: TextStyle(
                          color: blocState.themeData.primaryColor,
                          fontSize: 18.0,
                          fontWeight: FontWeight.bold),
                    ),
                    SizedBox(height: 30.0),
                    FloatingActionButton(onPressed: (){

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

    );
  }

}

```

##まとめ
本文はBLocでテーマや多言語の切替という多状態を説明しました。次回ではreduxパターンをご紹介しますので、引き続きお楽しみください。