FlutterのThemeをBlocを用いて切り替える

Flutterにて複数のThemeをUIから切り替える方法について、[この記事](https://resocoder.com/2019/08/09/switch-themes-with-flutter-bloc-dynamic-theming-tutorial-dark-light-theme/)を紹介することで説明します。

# 概要
以下のようにBlocを用いた状態管理によりThemeDataを変更します。

- 複数のThemeDataを定義しておく
- ThemeDataを切り替えるBlocを作成する
    - ThemeDataを切り替えるButtonを押した際にEventをBlocに送る
    - BlocがThemeDataを持つStateをViewに通知する
- MaterialAppのthemeプロパティがStateのThemeDataを参照する




# 複数のThemeDataを定義しておく
このサンプルでは以下の4種類のThemeを切り替えます。お好みで増やすことも可能です。
ThemeDataのプロパティも必要に応じて増やしてください。

```dart
// ui/global/app_themes.dart
enum AppTheme {
  GreenLight,
  GreenDark,
  BlueLight,
  BlueDark,
}

// 4つのTheeDataを作成する
final appThemeData = {
  AppTheme.GreenLight: ThemeData(
    brightness: Brightness.light,
    primaryColor: Colors.green,
  ),
  // 省略
}
```

# ThemeDataを切り替えるBlocを作成する
```theme_event.dart
// 抜粋
// 注: 抽象クラスThemeEventはEquatableをextendsしている
// 4つのAppTemeのうちで選択されたものをBlocに通知する動きをする
class ThemeChanged extends ThemeEvent {
  final AppTheme theme;

  ThemeChanged({
    @required this.theme,
  }) : super([theme]);
}
```

```theme_state.dart
// 抜粋
// 選択されたAppThemeに基づくThemeDataをUIに通知する動きをする
@immutable
class ThemeState extends Equatable {
  final ThemeData themeData;

  ThemeState({
    @required this.themeData,
  }) : super([themeData]);
}
```

```theme_bloc.dart
// 抜粋
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
  ThemeBloc(ThemeState initialState) : super(initialState);

  @override
  Stream<ThemeState> mapEventToState(
    ThemeEvent event,
  ) async* {
    if (event is ThemeChanged) {
      // Eventで受け取ったAppThemeに基づくThemeDataを通知する
      yield ThemeState(themeData: appThemeData[event.theme]);
    }
  }
}
```

## ThemeDataを切り替えるButtonを押した際にEventをBlocに送る
```preference_page.dart
// 抜粋
return Card(
  color: appThemeData[itemAppTheme].primaryColor,
  child: ListTile(
    title: Text(
      itemAppTheme.toString(),
      style: appThemeData[itemAppTheme].textTheme.bodyText2,
    ),
    onTap: () {
      // 選択されたAppThemeをBlocに流す
      // 
      BlocProvider.of<ThemeBloc>(context).add(
        ThemeChanged(theme: itemAppTheme),
      );
    },
  ),
);
```




# MaterialAppのthemeプロパティがStateのThemeDataを参照する
```main.dart
// 抜粋
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // BlocProviderでThemeBlocをWidget tree挿入する
    return BlocProvider(
      create: (BuildContext context) => ThemeBloc(
        // 初期値を設定する
        ThemeState(themeData: appThemeData[AppTheme.GreenLight]),
      ),
      child: BlocBuilder<ThemeBloc, ThemeState>(
        builder: _buildWithTheme,
      ),
    );
  }

  Widget _buildWithTheme(BuildContext context, ThemeState state) {
    return MaterialApp(
      title: 'Material App',
      // themeとしてStateのthemeDataを反映させる
      theme: state.themeData,
      home: HomePage(),
    );
  }
}
```

```home_page.dart
// 抜粋
body: Center(
  child: Container(
    child: Text(
      'Home',
      // styleとしてBlocから通知されたStateのthemeを呼び出す
      style: Theme.of(context).textTheme.headline4,
    ),
  ),
),
```


# まとめ
flutter_blocを用いた状態管理によりThemeDataを変更しました。
完成したコードは[こちらのresocoderさんの記事](https://resocoder.com/2019/08/09/switch-themes-with-flutter-bloc-dynamic-theming-tutorial-dark-light-theme/)にありますので参照してください。

resocoderさんのコードではThemeDataは永続化されませんので、必要に応じて永続化をしてください。記事ではpreferences、SEMBAST、Moor等を勧めています。

またresocoderさんの記事ではflutter_blocのバージョンが古いものになっています。
私の書いた記事はflutter_bloc6.1.1を用いて書いたものになります。適宜バージョンにあった形に直してお使いください。