tech

Flutterでダークモードをスマートに実装する

· 12min · Flutter, Dart, Theme
index

はじめに

スマートフォンユーザーにとって、ダークモードはもはや標準機能のひとつ。 Flutterはこれを非常にスマートに扱える仕組みを持っています。

本記事では、以下の2点をコンパクトにまとめます。

  1. OSの設定にあわせてダーク/ライトを自動で切り替える
  2. アプリ内でユーザーが手動で切り替えられるようにする

OS設定に追従させる

もっともシンプルな実装は、MaterialAppthemedarkThemeを渡すことです。

MaterialApp(
  theme: ThemeData.light(useMaterial3: true),
  darkTheme: ThemeData.dark(useMaterial3: true),
  themeMode: ThemeMode.system, // OSの設定に従う
  home: const HomePage(),
);

themeModeには次の3つが指定できます。

挙動
ThemeMode.systemOS設定に追従(既定)
ThemeMode.light常にライト
ThemeMode.dark常にダーク

カスタムテーマの定義

単にThemeData.light()を使うだけでなく、色やフォントもカスタマイズしておくとアプリの個性が出ます。

final lightTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.indigo,
    brightness: Brightness.light,
  ),
  textTheme: const TextTheme(
    bodyMedium: TextStyle(fontFamily: 'NotoSansJP'),
  ),
);

final darkTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.indigo,
    brightness: Brightness.dark,
  ),
);

ColorScheme.fromSeedは1色指定するだけで、Material 3基準のカラーパレットを自動生成してくれる優れものです。

ユーザーが手動で切り替えられるようにする

OS設定に追従するだけでなく、アプリ内で切り替えたいケースも多いと思います。 ここではシンプルにValueNotifierで実装してみます(Riverpodやbloc、好きな状態管理ツールに置き換えてOK)。

final themeModeNotifier = ValueNotifier<ThemeMode>(ThemeMode.system);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: themeModeNotifier,
      builder: (_, mode, __) {
        return MaterialApp(
          theme: lightTheme,
          darkTheme: darkTheme,
          themeMode: mode,
          home: const HomePage(),
        );
      },
    );
  }
}

切り替え用のトグルをどこかに置きます。

Switch(
  value: themeModeNotifier.value == ThemeMode.dark,
  onChanged: (isDark) {
    themeModeNotifier.value =
        isDark ? ThemeMode.dark : ThemeMode.light;
  },
);

永続化する

ユーザーが選んだテーマを記憶させたい場合はshared_preferencesなどで永続化します。

Future<void> saveThemeMode(ThemeMode mode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('themeMode', mode.name);
}

Future<ThemeMode> loadThemeMode() async {
  final prefs = await SharedPreferences.getInstance();
  final value = prefs.getString('themeMode') ?? 'system';
  return ThemeMode.values.firstWhere((m) => m.name == value);
}

アプリ起動時にloadThemeModeを呼び出して、themeModeNotifierの初期値にするだけで完成です。

現在のテーマを判定する

Widgetの中で現在がダークモードかどうかを判定するときはTheme.of(context).brightnessを使います。

final isDark = Theme.of(context).brightness == Brightness.dark;

画像やアイコンの差し替えに使えます。

おわりに

Flutterのテーマシステムは本当によくできていて、themeModeの一行だけでダークモード対応が始められます。 個人的には、最初からColorScheme.fromSeedを使って色を一元管理しておくと、後からの変更にも強くなるのでおすすめです。

参考文献