Flutterで独自のカラー(Dynamic Color以外)を扱うときに便利なパッケージ作った!!
こんにちは、Rerurate_514と申します。
今回は独自でカラー定義するときに使えるパッケージを作ってみたのでそれを共有します。
名前はtheme_palette_generatorです。
使い方
install
インストールには、
flutter pub get theme_palette_generatorと入力します。
依存関係に、
- build_runner
があるので、そちらの導入もお願いします。
カラースキーマのプロパティ定義
まず、このパッケージを使用するには独自のカラー定義を集めたクラスを作る必要があります。書き口としてはfreezedとほぼ同じです。違う点としては、ThemeExtensionを継承している点と、freezedの各種アノテーションは存在しない点ですね。
import 'package:flutter/material.dart';
import 'package:theme_palette_generator/theme_palette_generator.dart';
part 'ファイル名.theme.g.dart';
sealed class クラス名 extends ThemeExtension<クラス名> with _$クラス名{
const factory AppColorScheme({
required Color 色名,
}) = _クラス名;
}プロパティの型はimport 'package:flutter/material.dart';のColorです。
partディレクティブの拡張子はtheme.g.dartです!
実際に定義してみるとこんな感じ。
import 'package:flutter/material.dart';
import 'package:theme_palette_generator/theme_palette_generator.dart';
part 'app_color_scheme.theme.g.dart';
@ThemePalette()
sealed class AppColorScheme
extends ThemeExtension<AppColorScheme>
with _$AppColorScheme {
const factory AppColorScheme({
required Color surfaceSuccess,
required Color surfaceWarning,
required Color brandBlue,
required Color secondaryBrandBlue,
required Color statusPending,
required Color statusProcessing,
required Color statusCompleted,
required Color statusFailed,
}) = _AppColorScheme;
}今回は例として、この8つのフィールドを定義してみました。
これを使用していきます。
そして、build_runnerを走らせます。
dart run build_runner buildちなみに自分はlib/core/theme/においてます。
下に続くコードもここにおいてます。
色の定義
上記のクラスを定義したら次は色を定義していきます。
import 'package:flutter/material.dart';
import 'package:rerurate_flutter_template/core/theme/app_color_scheme.dart';
const lightCustomColors = AppColorScheme(
surfaceSuccess: Color(0xFFE8F5E9),
surfaceWarning: Color(0xFFFFF3E0),
brandBlue: Color(0xFF0D47A1),
secondaryBrandBlue: Color(0xFFD3E4FF),
statusPending: Color(0xFF9E9E9E),
statusProcessing: Color(0xFF1976D2),
statusCompleted: Color(0xFF388E3C),
statusFailed: Color(0xFFD32F2F)
);
const darkCustomColors = AppColorScheme(
surfaceSuccess: Color(0xFF1B5E20),
surfaceWarning: Color(0xFFE65100),
brandBlue: Color(0xFF2196F3),
secondaryBrandBlue: Color(0xFFD3E4FF),
statusPending: Color(0xFF757575),
statusProcessing: Color(0xFF2196F3),
statusCompleted: Color(0xFF4CAF50),
statusFailed: Color(0xFFF44336),
);大体こんな感じでしょうか。
同じファイルにMaterialAppに渡すためのThemeDataも定義します。
class AppTheme {
static ThemeData get light => lightCustomColors.buildTheme(
ThemeData.light(useMaterial3: true),
);
static ThemeData get dark => darkCustomColors.buildTheme(
ThemeData.dark(useMaterial3: true),
);
}ここでbuildTheme()を使用して、テーマを生成します。引数はThemeDataです。
使うとき
まず、MaterialAppに渡すテーマを選択。実際はRiverpodとか使ってテーマを選択できるようにするでしょうが、今回は省略します。
return MaterialApp(
theme: AppTheme.light,
// darkTheme: AppTheme.dark,そして、ウィジェットで実際に色を使います。
色を使うときは、BuildContextを介して行います。
@override
Widget build(BuildContext context) {
final theme = context.themePalette;
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: theme.brandBlue, //themeを介してアクセス
),
child: Text("Tap"),
);
}これだけ!!!
用意したメソッドについて
copyWith
一部の色だけ変えたインスタンスを作成できます。多分使うことはないかも。。。
final modified = lightCustomColors.copyWith(brandBlue: Colors.red);lerp
アニメーション中にテーマを補間します。Flutterエンジンが呼ぶことを想定しているので、基本的に直接呼ぶことはありません。
toMap
ただ、Mapにしてくれるだけです。
final map = lightCustomColors.toMap();
print(map['brandBlue']); // Color(0xFF0D47A1)== / hashCode
同じ色の組み合わせなら同一と判定します。
lightCustomColors == lightCustomColors.copyWith() // truebuildTheme
buildThemeはThemeDataにカスタムカラーを注入したThemeDataを生成するメソッドです。引数として既存のThemeDataを受け取り、extensionsにカスタムカラーを追加したものを返します。
class AppTheme {
static ThemeData get light => lightCustomColors.buildTheme(
ThemeData.light(useMaterial3: true),
);
static ThemeData get dark => darkCustomColors.buildTheme(
ThemeData.dark(useMaterial3: true),
);
}技術
今回使用したのは、
この五つのパッケージです。
ビルド生成系パッケージを作るのにほぼ必須級のですね。
freezedで生成されたコードとにらめっこしたり、AIにcode_builderの使い方聞いたりしてようやくできました。
制作時間としては9時間ぐらいでしょうか?
mixinを使うという発想まで時間がかかりました。最初はextendsでやろうと思って実装していたのですが、そうするとユーザがクラスを作成するときに、どうしてもコンストラクタを排除できなかったので、ずっと考えてました。
import 'package:flutter/material.dart';
import 'package:theme_palette_generator/theme_palette_generator.dart';
part 'app_color_scheme.theme.g.dart';
@ThemePalette()
sealed class AppColorScheme extends _$AppColorScheme {
const factory AppColorScheme({
required Color surfaceSuccess,
required Color surfaceWarning,
required Color brandBlue,
required Color secondaryBrandBlue,
required Color statusPending,
required Color statusProcessing,
required Color statusCompleted,
required Color statusFailed,
}) = _AppColorScheme;
const AppColorScheme._(); //この部分が排除できなかった
}でなんとか迂回して、ファクトリだけで完結する実装になりました。
結果的には、コンストラクタを排除できたので満足。
.theme.g.dartの中身としてはこんな感じ。
classDiagram class ThemeExtension~T~ { <<Flutter SDK>> } class BuildContext { <<Flutter SDK>> } class AppColorScheme { <<interface / abstract>> } class _AppColorScheme { } class Mixin_AppColorScheme { <<mixin>> +copyWith() AppColorScheme +lerp() ThemeExtension~AppColorScheme~ +toMap() Map~String, Color~ +buildTheme() ThemeData } class AppColorSchemeBuildContextExtension { <<extension>> +themePalette AppColorScheme } ThemeExtension <|-- Mixin_AppColorScheme : extends ThemeExtension <|-- _AppColorScheme : extends Mixin_AppColorScheme <|-- _AppColorScheme : mixin AppColorScheme <|-- _AppColorScheme : implements BuildContext ..> AppColorSchemeBuildContextExtension : extends via BuildContext AppColorSchemeBuildContextExtension ..> AppColorScheme : fetches
これで不変性とconstの維持ができるってわけですね。
これ_クラスが
@immutable
class _AppColorScheme extends ThemeExtension<AppColorScheme>
with _$AppColorScheme
implements AppColorSchemeextendsとwithとimplements全部乗せでちょっとだけ面白いw
でもこうしないと理想が実現できなかった。
終わり
ブランドカラーの細かい管理や、Dynamic Colorでは対応しきれないデザインシステムを持つプロジェクトでは、こういった独自カラー定義の仕組みが役立つと思います。
ぜひ使ってみてください! それか、もっとよい方法があればぜひ教えてください!