[Flutter]ライトモードとダークモードの切替はRiverpodで実装しよう

# はじめに
Flutterのフレームワーク [Riverpod](https://riverpod.dev/ja/) では、状態の管理を適切に簡潔に行うことができます。
この記事ではライトモードとダークモード（以下、外観モードと呼びます）の切替を Riverpod 経由で設定する方法を説明し、外観モードの簡単な設定方法と Riverpod の簡単な使い方について説明したいと思います。

# この記事の出来上がるもの

![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653487/f285dbbe-d51e-d04c-403d-26cbfb31fb94.gif)

#  Riverpod の使用するための設定
 Riverpod には3種類ありますが、今回は一番シンプルな flutter_riverpod を使用します。

> ![スクリーンショット 2022-07-23 14.11.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653487/edc29b8a-109d-dcb1-ed5e-99914ffebb87.png)
[Riverpod公式ドキュメント](https://riverpod.dev/ja/docs/getting_started#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E7%A8%AE%E9%A1%9E)より引用


以下では flutter_riverpod の利用手引きをご紹介します。

### flutter_riverpod パッケージをインストールする
pubspec.yaml に以下の依存関係を記載して、ターミナルで`flutter pub get`を実行してください。
```yaml:pubspec.yaml
environment:
  sdk: ">=2.12.0 <3.0.0"
  flutter: ">=2.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.0.0-dev.9
  # flutter_riverpod のバージョンは、公式サイトから逐一確認してください。
```

### flutter_riverpod を import する
以下を main.dart に追加してください
```dart:main.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
```

### エントリーポイントに ProviderScope を追加する
地味に忘れがちですが、Flutterアプリのエントリーポイント `void main()` において `ProviderScope()` の宣言が必要になります。
 Riverpod を扱う際は忘れないようにしましょう......！

```dart:main.dart
void main() {
  runApp(
    // MyApp を ProviderScope で括ってあげる
    const ProviderScope(
      child: MyApp()
    )
  );
}
```




# ThemeModeにStateProviderを設定する

[ThemeMode](https://api.flutter.dev/flutter/material/MaterialApp/themeMode.html) プロパティは、 [MaterialApp](https://api.flutter.dev/flutter/material/MaterialApp-class.html) の theme と darkTheme の両方が提供されている場合にアプリで使用される外観モードを決定します。以下はThemeModeプロパティの持つ値です。

| ThemeMode | 外観設定 |
|:-:|:-:|
| .system | システム設定に従う |
| .light | ライトモード |
| .dark | ダークモード |

ThemeModeに対して Riverpod の [StateProvider](https://riverpod.dev/ja/docs/providers/state_provider/) を作ってあげると、外観モードの状態管理が簡単にできます。

```dart:main.dart
// デフォルトはシステム設定に従う(ThemeMode.system)ようにしておくといいかも
final themeModeProvider = StateProvider<ThemeMode>((ref)=>ThemeMode.system);
```


# refオブジェクトを取得してthemeModeの状態を管理する

ウィジェットの中で上述の themeModeProvider を扱うために、refオブジェクトを取得する必要があります。

そのために通常の [StatelessWidget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html) ではなく、[ConsumerWidget](https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ConsumerWidget-class.html) を用います。ConsumerWidget は StatelessWidget とほとんど変わりませんが、 [build](https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ConsumerWidget/build.html) の引数に [WidgetRef](https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/WidgetRef-class.html) を取るようになります。
WidgetRef(ref)オブジェクトに対してref.watch メソッドを使用すると任意のプロバイダーを監視することができます。
themeModeProviderを用いて`final themeMode = ref.watch(themeModeProvider.state);`と宣言すると任意のプロバイダーを監視するオブジェクトが扱えます。
実際の使用例は以下の通りです。

```dart:main.dart
// 1. ConsumerWidget を継承したクラスを作成する
class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  // 2. 引数に WidgetRef ref を取るようにする
  Widget build(BuildContext context, WidgetRef ref) {
    // 3. themModeProvider の ref オブジェクトを取得する
    final themeMode = ref.watch(themeModeProvider.state);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Brightness Demo',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      // 4. themeModeProvider の state を用いて themeMode を設定する
      themeMode: themeMode.state,
      home: const MyHomePage(),
    );
  }
}
```

# 外観モードを選択するUIを作る
上述の themeModeProvider を経由して、アプリの外観モードを選択するUIを作ります。
今回は [PopUpMenuButton](https://api.flutter.dev/flutter/material/PopupMenuButton-class.html) を用います。 PopupMenuButton の使い方は詳しくは説明しませんが、以下コードや PopupMenuButton のドキュメントを参考にしてください。

まずbuildメソッド内でrefオブジェクトを取得します。
`final themeMode = ref.watch(themeModeProvider.state);`

次にPopupMenuButtonのonSelectedで`themeMode.state`にThemeModeを入れてあげます。
`onSelected: (ThemeMode selectedThemeMode) => themeMode.state = selectedThemeMode,`

これだけで単純に代入を行うだけでアプリの外観モードが設定されるようになります。

```dart:main.dart
// 1. ConsumerWidgetを継承する
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  // 2. 引数に WidgetRef ref を取る 
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch() を宣言する
    final themeMode = ref.watch(themeModeProvider.state);
    return Scaffold(
      appBar: AppBar(
        title: const Text("Brightness Demo"),
        actions: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: PopupMenuButton<ThemeMode>(
              icon: const Icon(Icons.settings_brightness,),
              // themeMode.state に選択された 外観モード をセットする
              onSelected: (ThemeMode selectedThemeMode) => themeMode.state = selectedThemeMode,
              itemBuilder: (context) => <PopupMenuEntry<ThemeMode>>[
                const PopupMenuItem(
                  value: ThemeMode.system,
                  child: Text('システム設定に従う'),
                ),
                const PopupMenuItem(
                  value: ThemeMode.light, 
                  child: Text('ライトモード'),
                ),
                const PopupMenuItem(
                  value: ThemeMode.dark,
                  child: Text('ダークモード'),
                ),
              ],
            ),
          ),
        ],
      ),
      body: const Center(
        child: Text(
          "設定に応じてfontColorが入れ替わる", 
          style: TextStyle(fontSize: 30.0),
        ),
      ),
    );
  }
}

```


# コード全文

以上をまとめると以下のようになります。
これをコピペして動作確認してみてください。


```dart:main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

// アプリ全体の外観モードの状態を管理するプロバイダー
final themeModeProvider = StateProvider<ThemeMode>((ref)=>ThemeMode.system);

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider.state);
    return MaterialApp(
      // AppBarのactionsと被っちゃうので画面右上のDEBUG帯を無効化する
      debugShowCheckedModeBanner: false,
      title: 'Brightness Demo',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      themeMode: themeMode.state,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider.state);
    return Scaffold(
      appBar: AppBar(
        title: const Text("Brightness Demo"),
        actions: [
          Padding(
            padding: const EdgeInsets.all(10.0),
            child: PopupMenuButton<ThemeMode>(
              icon: const Icon(Icons.settings_brightness,),
              onSelected: (ThemeMode selectedThemeMode) => themeMode.state = selectedThemeMode,
              itemBuilder: (context) => <PopupMenuEntry<ThemeMode>>[
                const PopupMenuItem(
                  value: ThemeMode.system,
                  child: Text('システム設定に従う'),
                ),
                const PopupMenuItem(
                  value: ThemeMode.light, 
                  child: Text('ライトモード'),
                ),
                const PopupMenuItem(
                  value: ThemeMode.dark,
                  child: Text('ダークモード'),
                ),
              ],
            ),
          ),
        ],
      ),
      body: const Center(
        child: Text(
          "設定に応じてfontColorが入れ替わる", 
          style: TextStyle(fontSize: 30.0),
        ),
      ),
    );
  }
}

```


# 実行環境
```console:% flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.0.5, on macOS 12.4 21F79 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.69.1)
[✓] Connected device (2 available)
[✓] HTTP Host Availability
```