ThemeExtension を使用してプロジェクト独自のカラースキームやコンポーネントのスタイルを定義する。
はじめに
こんにちは!
株式会社アンドエーアイの荻野と申します!
今回は「ThemeExtension を使用してプロジェクト独自のカラースキームやコンポーネントのスタイルを定義する。」と題して記事を書いていこうと思います!
モチベーション
モバイルアプリを開発をしていると時折、
「ブランドイメージのため独自のデザインを使用している。」
「社内での効率的な開発のためにデザインシステムを独自のデザインシステムを使用している。」
といった理由で以下のようなことが起きます。
- クライアントからFigmaなどでデザインを提供される。
- 提供されたデザインのカラースキームやコンポーネントが独自のデザインシステムに準拠している。
- Flutter におけるテーマとプロパティの命名やその内容が噛み合わない。無理やり合わせようとするとチグハグなコードになってしまう。
- テーマの切り替えなどテーマの恩恵を受けることができない。
それが特に今後メンテナンスされないようなアプリであったりすれば、とりあえず定数などで定義してしまう方法でも問題は特に起きません。
しかし、大多数のアプリは今後継続的に保守・機能改善が行われていくことを前提としており、そもそも要件でテーマの切り替えが求められているような場合、それだけでは解決できません。
こういった場合にThemeExtensionを使用することで上記の問題を回避することができます。
ThemeExtension とは
簡単に言うと、FlutterのThemeDataにオリジナルの項目を追加できる機能です。
たとえばThemeDataには標準コンポーネントやカラースキームが含まれていますが、これに加えてオリジナルのコンポーネントや色の定義をすることができるようになります。
実装
実装環境
Flutter 3.29.2
ThemeExtension を定義する
ThemeExtensionを継承したクラスを実装します。
class MyColors extends ThemeExtension<MyColors> {
const MyColors({
required this.brandColor,
required this.background,
});
final Color brandColor;
final Color background;
@override
MyColors copyWith({
Color? brandColor,
Color? background,
}) {
return MyColors(
brandColor: brandColor ?? this.brandColor,
background: background ?? this.background,
);
}
@override
MyColors lerp(
MyColors other,
double t,
) {
return MyColors(
brandColor: Color.lerp(brandColor, other.brandColor, t) ?? brandColor,
background: Color.lerp(background, other.background, t) ?? background,
);
}
}
実装するクラスには、copyWithとlerpの二つのメソッドを実装する必要があります。
この二つは追加するクラスを他のプロパティと同様に取り扱うために必要なメソッドです。
特にlerpはライトテーマ/ダークテーマの切り替え時などにフェードアニメーションを使えるようにするために必要だったりします。
あとでテーマを切り替えられるように、lightとdarkのコンストラクタを定義します。
const MyColors.light()
: brandColor = Colors.blue,
background = Colors.white;
const MyColors.dark()
: brandColor = Colors.red,
background = Colors.black;
ThemeExtension を設定する
MaterialAppもしくはThemeに実装したThemeExtensionを含むThemeDataを渡します。
return MaterialApp(
title: 'Flutter Demo',
themeMode: mode,
theme: ThemeData(
extensions: [MyColors.light()],
),
darkTheme: ThemeData(
extensions: [MyColors.dark()],
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
この状態でThemeModeを切り替えると、通常のテーマの切り替えと同じように使用するMyColorsも切り替わることになります。
ThemeExtension を呼び出す
定義したテーマを使用する際はTheme.ofでcontextからThemeDataを取得し、extensionメソッドを使用することで実装したThemeExtensionにアクセスすることができます。
公式の実装を参考にし、実装したクラスにofメソッドを生やしておいてもより使いやすいかと思います。
Widget build(BuildContext context) {
final colors = Theme.of(context).extension<MyColors>()!;
return Scaffold(
backgroundColor: colors.background,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: colors.brandColor,
height: 100,
width: 100,
),

問題なく実装した色が使用されていることがわかります。
次に実際にThemeModeを切り替えてきちんと色が切り替わるかを確認します。
※切り替えは適宜SegmentedButtonとRiverpodを使用して処理しています。
問題なく切り替わり、アニメーションもきちんと適用されていることが確認できました。

最後に
今回はカラースキームのベースにThemeExtensionを実装してみましたが、その他にもタイポグラフィや、独自コンポーネントのスタイルも同様の方法で定義可能です。
一点デメリットとして、カラースキームなどのプロパティが多いExtensionを作成しようとするとどうしてもcopyWithやlerpの記述量が膨れ上がり、追加の際も複数箇所に変更を加える必要があるため、必ずしもこれが正解というわけではなさそうです。
自動生成パッケージなどがあれば検討し、実際の要件などを踏まえて使用するかどうかを決めていく必要がありそうです。
PR
アンドエーアイでは事業拡大のため、即戦力エンジニアを募集中!Flutterだけでなく、インフラ、Web、ネイティブ開発などの知識を持つ方も歓迎します。最新技術を追い、チームに積極的に貢献できる方をお待ちしています!
採用ページ
エンジニア採用ページ
参考資料
Discussion