🎨

FlutterアプリのカラースキームをAndroidの壁紙に合わせる

に公開

はじめに

最新のAndroidには、ユーザーが設定した壁紙の色に基づいてシステムのカラースキームを自動的に調整する機能が用意されています。

この機能はシステムアプリだけではなく、サードパーティのアプリでも利用できます。Flutterアプリでもこの機能を活用すれば、アプリの外観を壁紙に合わせてシステムと一貫性のあるデザインを提供できます。

以下で、この「FlutterアプリのカラースキームをAndroidの壁紙に合わせる」方法について説明します。テストは以下の環境で行っています。

  • Flutter 3.35.2
  • Android 16

大まかな流れ

Flutterアプリで壁紙に基づくカラースキームを実装するにはDynamicColorBuilderクラスを使用します。

このクラスは、システムのカラースキームを取得し、アプリのテーマに適用できるようにしてコールバック関数を呼び出します。

そこでこのコールバック関数の中でMaterialAppを構築することで、アプリからシステムのカラースキームを使用できるようになるわけです。

以下では、この考え方に基づいた実装例を示します。この実装例は、Flutterが自動生成するアプリに少し手を加えて、カラースキームを

  • 壁紙にあわせる
  • 壁紙にあわせない(固定)

の2パターンを切り替えられるようにしています。切り替えのためにSettingsクラスを用意し、Providerパッケージで管理します。

pubspec.yamlの編集

このプログラムでは次の二つのパッケージを使用します。flutter createを実行して生成したコードのpubspec.yamlに追記してください。

  • provider:
  • dynamic_color:
dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.5+1
  dynamic_color: ^1.8.1

main.dartの編集

プログラムの変更は小規模なので、main.dartにすべて書くことができます。。

以下のコードは、Flutterが自動生成するmain.dartをベースにしています。

Settingsクラスの追加

アプリのカラースキームを次の二つから選ぶことができるようにします。

  • アプリ固有の設定にする
  • 壁紙にあわせる

この選択の状態を保持するために、Settingsクラスを追加します。

小規模なサンプルアプリですしグローバル変数を使っても良いのですが、ここは良いお作法に従うことにします。

// 特にひねりの無いChangeNotifier
class Settings with ChangeNotifier {
  bool _isWallpaperBased = true;

  bool get isWallpaperBased => _isWallpaperBased;

  set isWallpaperBased(bool value) {
    if (_isWallpaperBased != value) {
      _isWallpaperBased = value;
      notifyListeners();
    }
  }
}

このクラスのオブジェクトを、main()関数でChangeNotifierProviderを使ってアプリケーションと結びつけます。

void main() {
  runApp(
    // SettingsオブジェクトをProviderとしてアプリに提供
    ChangeNotifierProvider(
      create: (context) => Settings(),
      child: const MyApp(),
    ),
  );
}

GUIに設定ボタンを追加

カラースキームをアプリ固有にするか壁紙にあわせるかを切り替えるためのボタンをGUIに追加します。

追加する場所は、MyHomePageクラスのbuild()メソッドです。切り替えにはSegmentedButtonウィジェットを使用しました。

切り替えをonSelectionChangedコールバックで検知し、Providerで所得したSettingsオブジェクトのisWallpaperBasedプロパティを更新します。

  
  Widget build(BuildContext context) {
    Settings settings = Provider.of<Settings>(context);

    // …… 中略

            // このSegmentedButtonが追加するUI部分
            SegmentedButton<bool>(
              segments: const <ButtonSegment<bool>>[
                ButtonSegment<bool>(
                  value: false,
                  label: Text('アプリ'),
                  icon: Icon(Icons.home),
                ),
                ButtonSegment<bool>(
                  value: true,
                  label: Text('壁紙'),
                  icon: Icon(Icons.wallpaper),
                ),
              ],
              selected: <bool>{settings.isWallpaperBased},
              // ここでSettingsオブジェクトのプロパティを更新
              onSelectionChanged: (Set<bool> newSelection) {
                settings.isWallpaperBased = newSelection.first;
              },
            ),

    // …… 中略

  }

DynamicColorBuilderでMaterialAppを包む

ここが肝心かなめの部分です。

修正点を説明する前に、flutter createで生成されたMyAppクラスのbuild()メソッドを見てみましょう。これはMyHomePage()を画面としてMaterialApp()を作るだけの単純なメソッドです。themeプロパティでカラースキームを指定しています。

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }

このコードのMaterialAppをDynamicColorBuilderで包みます。DynamicColorBuilderのbuilderプロパティにコールバック関数を指定し、その中でMaterialAppを構築します。

  
  Widget build(BuildContext context) {
    // ProviderからSettingsオブジェクトを取得
    Settings settings = Provider.of<Settings>(context);

    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        // ここでカラースキームを決定する。
        ColorScheme lightColorScheme;
        ColorScheme darkColorScheme;
        // システムのダイナミックカラーを取得できて、かつユーザーがその利用を望んでいるか。
        if (lightDynamic != null &&
            darkDynamic != null &&
            settings.isWallpaperBased) {
          // 壁紙に基づくカラースキームを利用する。
          lightColorScheme = lightDynamic.harmonized();
          darkColorScheme = darkDynamic.harmonized();
        } else {
          // アプリ固有のカラースキームを利用する。
          lightColorScheme = ColorScheme.fromSeed(seedColor: Colors.deepPurple);
          darkColorScheme = ColorScheme.fromSeed(
            seedColor: Colors.deepPurple,
            brightness: Brightness.dark,
          );
        }

        // 生成したカラースキームに基づいてアプリを構築する。
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(colorScheme: lightColorScheme, useMaterial3: true),
          darkTheme: ThemeData(
            colorScheme: darkColorScheme,
            useMaterial3: true,
          ),
          themeMode: ThemeMode.system,
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      },
    );
  }

大規模な変更が行われたように見えますが、要点は次の3つです。

  1. DynamicColorBuilderでMaterialAppを包む。
  2. DynamicColorBuilderのコールバック関数内で、壁紙に基づくカラースキームを取得し、Settingsオブジェクトの状態に応じてカラースキームを選択する。
  3. MaterialAppのthemedarkThemeプロパティに選択したカラースキームを設定する。

この要点さえ押さえれば、上のコードは特に難しくありません。

全プログラム

以上の変更を加えた全プログラムをgistに公開しています。

まとめ

言葉にすると面倒に感じますが、実際に手を動かすとあっさりと実装が終わってしまいます。壁紙にあわせてアプリの雰囲気が変わるのは、なかなか良い気分です。

なお、DynamicColorBuilderは動作中に壁紙の変更を検知するとビルド・ツリーを根元から再構築します。そのため、GUI上で作業中の状態が失われることに注意してください。

壁紙の更新中はアプリがバックグラウンドに回るため、これはユーザーから見るとよくある「バックグラウンドに回したアプリが初期化されてしまう」状態です。これがユーザー体験を大きく損なう恐れがあるときには、作業中の状態を保存しておくなどの対策が必要です。

Discussion