🐬

【Flutter】Material Theme Builder で好みの色をアプリに組み込む

2022/12/15に公開

https://qiita.com/advent-calendar/2022/flutter

はじめに

個人開発をするとき、いつもデザイン適用に悩みます。とくに配色。

デザインを知らないエンジニアでも、なんとかセンスのいい配色を適用してアプリをかっこよく見せたいものです。そんな中 Flutter でよく耳にする Material Design について色々調べていたところ、いい感じの色を選ぶだけですぐに Flutter アプリに適用できるサービスを見つけました。

https://m3.material.io/theme-builder#/dynamic

本記事ではこちらのサービスを使い、直感的に選択した色を Flutter アプリに適用するまでを記した記事になります。

余談

つい先日に『センスは知識からはじまる』という書籍を読みました。
センスは感覚ではなく知識の集約だから、才能と決めつけず勉強しようねといった内容です。

勉強になる面白い本でした。

https://www.amazon.co.jp/センスは知識からはじまる-水野学-ebook/dp/B00LIQMVLQ/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=&sr=


Material Design

Flutter を触っているとやたら Material Design という言葉を聞きますよね。

Material Design は 2014 年に Google が提唱したデザインシステムで、その名の通りマテリアル(=物質的)な、現実の物質の法則に則った直感的なデザインを表現しています。

そういった概念を Material Design のガイドライン に落とし込み、ガイドラインに準拠して用意された UI ブロックやその状態や伝達の仕組みを Material Components (MDC) というフレームワークとして提供しています。

MDC は Flutter にも提供されていて、AppBarBottomNavigationBarDrawer などの Widget を実装するだけでオシャレな UI が実現できるのはこのためです。

Material Design や Material Design for Flutter はこのあたりが参考になると思います。


Material Design の Style には Icon や Elevation、Typography などさまざまな項目がありますが、今回は『Color』に着目しています。

Material Design の Color に関する説明はこちらです。

https://m3.material.io/styles/color/overview

TL;DR

  1. Material Theme Builderで好きな配色を選ぶ。
  2. Export から「Flutter(Dart)」を選択する。
  3. ファイルを unzip し、color_schemes.g.dartmain.g.dartを lib 下にドラッグアンドドロップする。
  4. main.g.dartに倣ってmain.dartを書き換える。

Material Design Builder for Flutter

テーマビルダーを適用するプロジェクトです。
デフォルトの counter app にコンポーネントを追加しただけです。

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

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const MyHomePage(title: 'm3'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  double _currentSliderValue = 20;
  bool _light = true;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(16),
        ),
        margin: const EdgeInsets.symmetric(horizontal: 40, vertical: 120),
        padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 64),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            const TextField(
              obscureText: true,
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Password',
              ),
            ),
            ElevatedButton(
                onPressed: () {}, child: const Text('ElevatedButton')),
            Slider(
              value: _currentSliderValue,
              max: 100,
              divisions: 5,
              label: _currentSliderValue.round().toString(),
              onChanged: (double value) {
                setState(() {
                  _currentSliderValue = value;
                });
              },
            ),
            Switch(
              value: _light,
              activeColor: Colors.red,
              onChanged: (bool value) {
                setState(() {
                  _light = value;
                });
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
      bottomNavigationBar: NavigationBar(
        destinations: const [
          Icon(Icons.list),
          Icon(Icons.home),
          Icon(Icons.settings),
        ],
      ),
    );
  }
}

Material Theme Builder にアクセスし、『Custom』からサイドバーの Core colors の 4 色をピッカーから選択します。


Material Theme Builder

Core colors

Accent color 3 色と Neutral color 1 色を選びます。

色の役割のあれこれは こちら に書いてありますが、正直難しいです。これ理解して柔軟に扱えるデザイナーさんがいたらめっちゃ心強いですね。

Primary color

アプリの画面やコンポーネントで最も頻繁に表示される色です。

Secondary color

UI の中であまり目立たない部品に使われ、色の表現を広げます。
こちらはオプションで、テーマビルダーにおいても Primary color を選択すると自動で調整されます。

Tertiary color

Primary color と Secondary color のバランスをとったり、注目度を高めるためのアクセントとして用いたり。
自由に色を設定して製品の色彩表現の幅を広げることを目的としています。

Neutral color

面や背景に用いられ、テキストやアイコンを強調する色としても用いられる。



好みの色を選択する


色を選択し終えたら右上の『Export』から Flutter(Dart)をクリックし、zip をダウンロードし解凍します。

color_schemes.g.dartmain.g.dartがフォルダに含まれているので、そのままプロジェクトの/lib配下に移動します。



Flutter の他にも kotlin や CSS ファイルとしても出力できる


この時点でmain.g.dartは動作するものとなっており、適用した色を確認することができます。今回はすでにmain.dartを編集しているので、main.g.dartの内容を真似て適用してあげます。

import 'package:flutter/material.dart';
+ import 'color_schemes.g.dart';
return MaterialApp(
+ theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
+ darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
  home: const MyHomePage(title: 'm3'),
);

また、color_schemes.g.dartの ColorScheme class でエラーが出ているので該当箇所をコメントアウトします。



エラー箇所をコメントアウトする


エミュレータを再読み込みしてあげると、先程選択したテーマが反映されていると思います。

一気に counter app のデフォルト感がなくなりましたね。

また、useMaterial3: trueというように Material Design 3(Material Design の最新版)を適用しているので、ボタンの形状やAppBarの色使いなどが若干変わっています。

ダークモード

color_schemes.g.dartの記述でお気づきかもしれませんが、ダークモード用の配色も用意されています。

darkColorScheme
const darkColorScheme = ColorScheme(
  brightness: Brightness.dark,
  primary: Color(0xFFFFB2B9),
  onPrimary: Color(0xFF67001F),
  primaryContainer: Color(0xFF91002F),
  onPrimaryContainer: Color(0xFFFFDADC),
  secondary: Color(0xFFE5BDBF),
  onSecondary: Color(0xFF43292C),
  secondaryContainer: Color(0xFF5C3F42),
  onSecondaryContainer: Color(0xFFFFDADC),
  tertiary: Color(0xFFE8C08E),
  onTertiary: Color(0xFF442B06),
  tertiaryContainer: Color(0xFF5D411B),
  onTertiaryContainer: Color(0xFFFFDDB6),
  error: Color(0xFFFFB4AB),
  errorContainer: Color(0xFF93000A),
  onError: Color(0xFF690005),
  onErrorContainer: Color(0xFFFFDAD6),
  background: Color(0xFF201A1A),
  onBackground: Color(0xFFECE0E0),
  surface: Color(0xFF201A1A),
  onSurface: Color(0xFFECE0E0),
  surfaceVariant: Color(0xFF524344),
  onSurfaceVariant: Color(0xFFD7C1C2),
  outline: Color(0xFF9F8C8D),
  onInverseSurface: Color(0xFF201A1A),
  inverseSurface: Color(0xFFECE0E0),
  inversePrimary: Color(0xFFB61E44),
  shadow: Color(0xFF000000),
  surfaceTint: Color(0xFFFFB2B9),
  // outlineVariant: Color(0xFF524344),
  // scrim: Color(0xFF000000),
);


ThemeDataで読み込むカラースキームをダークモードの配色にします。

return MaterialApp(
- theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
+ theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
  home: const MyHomePage(title: 'm3'),
);


ダークモードの適用

これだけでダークモード実装の工数がかなり省けますね。


適用する色のロールの変更もかんたんです。

例えば FloatingActionButton の色を アクセントカラーにしたいときは、以下のようにテーマを定義している直近の先祖(この場合 MyApp の MaterialApp)のテーマデータを取得し、その中のtertiaryContainerの色を適用してあげます。

floatingActionButton: FloatingActionButton(
  onPressed: _incrementCounter,
  tooltip: 'Increment',
+ backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
  child: const Icon(Icons.add),
),


FAB の色を変更

おわりに

はじめてアドベントカレンダーに参加してみましたが、同じ技術を扱う開発者の方との一体感を感じられる良いイベントですね。

Flutter を触り始めてから半年ほど経ちますが、個人開発やコミュニティ(主に Twitter)の盛り上がり、多くのイベント開催など触れていて本当に楽しい技術です。来年はもっとコミュニティの発展や個人開発に力を入れたいなぁ。

zenn 記事もまだまだ書いていく予定ですので、今後もよろしくお願いします。
よいクリスマス、よいお年を!

参考

GitHubで編集を提案

Discussion