🎨

Material3 ThemeData攻略[ダークモード対応・コード有り]

2024/09/06に公開

Flutter(Dart)でmaterial3で、ThemeDataを使ったUIデザインの構築を見ていきます。
ダークモードにも対応します。

こんな人におすすめ

・Flutterでダークモード対応をしたい
・material3の、seedColorの使い方を知らない
・文字をアプリで統一したい
・Widgetのデザインをアプリで統一したい

⭐️ 上記がThemeDataを正しく使うことで、簡単に実装できます。

ThemeDataのコードをざっくり見る

今回、説明で使うコードは以下です。
以下では、こちらのコードの詳細を追っていきます。

   return MaterialApp(
      title: 'Theme Tips',
      theme: themeLight(),
      darkTheme: themeDark(),
      themeMode: themeMode,
      home: const Home(),
    );
// themeDarkも同様に実装してください。
ThemeData themeLight() {
  const primary = Color.fromARGB(255, 56, 106, 31);
  // テーマの基本設定
  final base = ThemeData(
    fontFamily: GoogleFonts.kiwiMaru().fontFamily,
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      brightness: Brightness.light,
      seedColor: primary,
      primary: primary,
      surface: primary,
      error: Colors.blue,
      surfaceContainerLow: Colors.orange,
    ),
    textTheme: const TextTheme(
      bodyMedium: TextStyle(fontSize: 18),
      headlineMedium: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
    ),
  );

  return base.copyWith(
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.purple,
        foregroundColor: Colors.white,
      ),
    ),
    floatingActionButtonTheme: base.floatingActionButtonTheme.copyWith(
      backgroundColor: Colors.black,
      foregroundColor: Colors.white,
    ),
    pageTransitionsTheme: const PageTransitionsTheme(
      builders: {
        TargetPlatform.android: CupertinoPageTransitionsBuilder(),
        TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
      },
    ),
   // scaffoldBackgroundColor: Colors.green,
  );
}

画面のコード

画面のコード
class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Theme Tips",
            style: Theme.of(context).textTheme.headlineMedium),
      ),
      body: SingleChildScrollView(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // themeの切り替え
                const SettingsSwitchTile(
                  title: "theme切替え",
                ),
                Text("・seedColorによる自動生成色が適応される例",
                    style: Theme.of(context).textTheme.headlineMedium),
                SizedBox(height: 16),
                Text("secoundary",
                    style: TextStyle(
                        color: Theme.of(context).colorScheme.secondary)),
                Text("tertiary",
                    style: TextStyle(
                        color: Theme.of(context).colorScheme.tertiary)),

                Text("onSurface",
                    style: TextStyle(
                        color: Theme.of(context).colorScheme.onSurface)),
                Text("errorContainer",
                    style: TextStyle(
                        color: Theme.of(context).colorScheme.errorContainer)),

                SizedBox(height: 16),

                Text("・ColorSchemeのカスタマイズが適応される例",
                    style: Theme.of(context).textTheme.headlineMedium),
                SizedBox(height: 16),
                const TextField(
                  decoration: InputDecoration(
                    labelText: "Error 色を青にカスタマイズ",
                    hintText: "Input something",
                    errorText: "Error Message",
                  ),
                ),
                const SizedBox(height: 16),

                const Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: const Text("surfaceContainerLowをオレンジにカスタマイズ"),
                  ),
                ),

                SizedBox(height: 16),

                /// 以下は、app.dart で設定したテーマが適応される例
                ///
                /// error が適応される例
                Text("・ウィジェットのカスタマイズが適応される例",
                    style: Theme.of(context).textTheme.headlineMedium),
                SizedBox(height: 16),

                const SizedBox(height: 16),

                // elevatedButtonの設定が適応される例
                ElevatedButton(
                  onPressed: () {},
                  child: const Text("ElevatedButtonカスタマイズ"),
                ),

                // FloatingActionButtonの設定が適応される例
                FloatingActionButton(
                  onPressed: () {
                    showDialog(
                      context: context,
                      builder: (BuildContext context) {
                        return AlertDialog(
                          title: const Text("Dialog Title"),
                          content: const Text("This is a simple dialog."),
                          actions: <Widget>[
                            TextButton(
                              child: const Text("Close"),
                              onPressed: () {
                                Navigator.of(context).pop(); 
                              },
                            ),
                          ],
                        );
                      },
                    );
                  },
                  // floatingActionButtonの設定が適応される例
                  child: Text("FAB"),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
class SettingsSwitchTile extends ConsumerStatefulWidget {
  final String title;
  const SettingsSwitchTile({
    Key? key,
    required this.title,
  }) : super(key: key);
  @override
  _SettingsSwitchTileState createState() => _SettingsSwitchTileState();
}

class _SettingsSwitchTileState extends ConsumerState<SettingsSwitchTile> {
  @override
  Widget build(BuildContext context) {
    final themeNotifier = ref.read(themeColorRepositoryProvider.notifier);

    return ListTile(
      contentPadding:
          const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
      title: Text(widget.title),
      trailing: Switch(
        autofocus: true,
        value: Theme.of(context).brightness == Brightness.dark,
        onChanged: (bool newValue) {
          themeNotifier.setState(
            newValue ? ThemeMode.dark : ThemeMode.light,
          );
        },
      ),
    );
  }
}

カスタムフォントを適応する

以下で、全体のカスタムフォントが適応できます。
ちなみに、kiwiMaruかわいいのでおすすめです。

final base = ThemeData(
/// アプリのフォントを指定
fontFamily: GoogleFonts.kiwiMaru().fontFamily,

seedColorを理解する

まず、初めに、ColorSchemeを設定する場合、必須のプロパティである seedColorを理解する必要があります。
これは、一言でいうと、基準となる色を設定して関連する色を自動で配色する仕組みです。

以下の表をご覧ください。

// 以下のprimay color ()
const primary = Color.fromARGB(255, 56, 106, 31);
...
seedColor: primary,

以下のように、色が生成されていることがわかります。

https://m3.material.io/styles/color/roles#08bb3941-6f24-4749-920e-e78cdc938f93

自動で配色された色をカスタマイズしたい場合

例えば、極端ですが、上記の表では、errorの色は赤ですよね。
こちら、青に設定するには、どうすればいいでしょうか。

カスタマイズしてみましょう。

  colorScheme: ColorScheme.fromSeed(
      brightness: Brightness.light,
     ...
      error: Colors.blue,  ← ここで青に設定する。

適応される色を、ざっくり知りたい場合は以下の記事をご参照ください。
※backgroudなど、現在は使われていないものもあります。
https://zenn.dev/gen_kk/articles/cc538ffa392922

Widgetのコード内部を見て、色を決定する。

上記で、完全に色をコントロールするのは正直難しいです。
ですので、コードの内部を見ていく必要があります。

以下は、Cardのbackgroudがどうなっているかみてみましょう。

  /// The card's background color.
  ///
  /// Defines the card's [Material.color].
  ///
  /// If this property is null then the ambient [CardTheme.color] is used. If that is null,
  /// and [ThemeData.useMaterial3] is true, then [ColorScheme.surfaceContainerLow] of
  /// [ThemeData.colorScheme] is used. Otherwise, [ThemeData.cardColor] is used.
  final Color? color;

背景色が null の場合、以下の順序で色が適用されます。

カードテーマの color プロパティ。
ThemeData.useMaterial3 が true の場合、ColorScheme.surfaceContainerLow の色。
上記が全て null の場合、ThemeData.cardColor が使われます。

となっていました。 したがって、surfaceContainerLowを設定してあげればいいことになります。
上記の表をご覧ください。なにも設定していない場合は、surfaceContainerLowは薄い緑ですよね。

オレンジにしてみます。

    colorScheme: ColorScheme.fromSeed(
      ...
      surfaceContainerLow: Colors.orange,

文字を統一しよう

TextThemeにより文字の統一が可能です。
例えば、ヘッドラインや小見出しなどの文字を統一できます。

以下のような感じです。
※ ちなみに、 Text("") は、bodyMediumが適応されているので、以下で統一可能です。
今回は、大きめにしましたが仕様書とかでは大体14あたりではないでしょうか。

// デフォルトのテキストスタイル
bodyMedium: TextStyle(fontSize: 18),
// ヘッドラインのテキストスタイル
headlineMedium: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),

Widgetに適応されるテーマを設定しよう

以下では、colorSchemeで自動で配色される色ではなく、Widget別にデザインをカスタマイズする例を挙げます。

たとえば、ElevatedButtonのテーマを設定する例です。
背景を紫にして、文字の色を白くしました。

return base.copyWith(
// ElevatedButtonのテーマを設定する例
elevatedButtonTheme: ElevatedButtonThemeData(
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.purple,
        foregroundColor: Colors.white,
  ),
),

アプリの背景色を変えよう

scaffoldBackgroundColor: Colors.green,

極端ですが緑にもできます。

まとめ

仕様が決まっていれば、割と簡単にThemeDataを設定できそうですね!
参考になれば幸いです!!

Discussion