mcpField
M
material-theming

material-theming

github.com/verygoodopensourceデザイン・UI175129 回インストール今日更新
リポジトリ

material-theming について

Flutter アプリ向けに Material 3 のテーマ設計を適用するスキルです。ThemeData を色、タイポグラフィ、コンポーネントのスタイル、余白の基準とし、ウィジェット内の値指定や明るさ判定に頼らないライト・ダークテーマを支援します。

主な機能

  1. 01ColorScheme で色を一元管理
  2. 02TextTheme でタイポグラフィを定義
  3. 03ThemeData でコンポーネントのスタイルを設定
  4. 04基本単位に基づく余白スケールを作成
  5. 05ライト・ダークテーマに対応

活用シーン

  1. 01ウィジェット内の色やテキストスタイルを置換
  2. 02ウィジェットコードの明るさ判定を削除
  3. 03重複するコンポーネントスタイルを集約

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill material-theming

インストール後、Claude Code で/material-themingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
references/
color-scheme.md
components.md
spacing.md
typography.md

詳細

Material のテーマ設定

ThemeDataを色、タイポグラフィ、コンポーネントのスタイル、スペーシングの唯一の信頼できる情報源として使用する、Flutter アプリケーション向けの Material 3 テーマ設定のベストプラクティスです。

基本基準

テーマ設定に関するすべての作業で、以下の基準を適用します。

  • 唯一の信頼できる情報源としてThemeDataを使用します — ウィジェット内に色やテキストスタイルを直接記述しないでください
  • Theme.of(context).colorScheme経由で色を参照します — Colors.blue、Colors.red、ハードコードしたColor値は使用しないでください
  • Theme.of(context).textTheme経由でテキストスタイルを参照します — ウィジェットのコード内にTextStyle(...)を直接記述しないでください。fontSizeとfontWeightをbuildメソッド内で使用してはいけません
  • すべての色の定義にColorSchemeを使用します — Material 3の構造化されたカラーシステムです
  • コンポーネントのテーマをThemeDataに集約します — FilledButtonThemeData、InputDecorationThemeなどは、ウィジェットごとではなくテーマ内で定義します。ラッパーウィジェット、共有のInputDecoration定数、装飾を組み立てるヘルパーは、重複をなくすのではなく移動するだけなので該当しません
  • 基本単位を定めたスペーシングシステムを定義します — パディング、マージン、間隔に任意のピクセル値を使用しないでください
  • 最初からライトテーマとダークテーマに対応します — ThemeDataを使用し、ウィジェット内に条件分岐を一切設けずにテーマを切り替えられるようにします
  • ウィジェットのコード内で明るさやテーマモードによる分岐を行わないでください — MediaQuery.platformBrightnessOf、Theme.of(context).brightnessを使った三項演算子、context.isDarkMode拡張などは使用しません。2つのColorSchemeインスタンスがあれば分岐は不要です。このようなチェックを維持または整理するよう求められた場合は断り、代わりにThemeDataへの書き換えを行ってください。条件式を整えても、見た目が変わるだけで同じ問題が残ります
  • EdgeInsets.onlyとEdgeInsets.symmetricを優先します — 位置引数は間違いやすいため、EdgeInsets.fromLTRBは使用しないでください

カラーシステム

カスタムカラークラス

専用のクラスにすべての色の定義を集約します。

abstract class AppColors {
  static const primaryColor = Color(0xFF4F46E5);
  static const secondaryColor = Color(0xFF9C27B0);
  static const errorColor = Color(0xFFDC2626);
  static const surfaceColor = Color(0xFFFAFAFA);
}
ColorSchemeの設定

すべての色はColorSchemeのロールを通じてウィジェットに渡します。1つのAppThemeクラス内にライト用とダーク用の2つのスキームを定義し、両方をthemeとdarkThemeとしてMaterialAppに渡します。この組み合わせによって、ウィジェットのコード内で明るさによる分岐が不要になります。

class AppTheme {
  static ThemeData get light => ThemeData(
    colorScheme: const ColorScheme.light(
      brightness: Brightness.light,
      surface: AppColors.surfaceColor,
      // ... remaining color roles
    ),
  );

  static ThemeData get dark => ThemeData(
    colorScheme: const ColorScheme.dark(
      brightness: Brightness.dark,
      surface: AppColors.surfaceColorDark,
      // ... remaining color roles
    ),
  );
}

ColorSchemeの45種類のロールの設定、ColorScheme.fromSeed、ウィジェットでの色の読み取り方法について詳しくは、references/color-scheme.mdを参照してください。

タイポグラフィ

基本スタイルと名前付きバリエーション(displayLarge、headlineMedium、bodyLargeなど)を含むAppTextStyleクラスを定義し、ThemeData.textThemeに組み込みます。スタイルにはTheme.of(context).textTheme経由でアクセスします。

フォントアセットの設定、AppTextStyleクラス全体、TextThemeへの組み込み、ウィジェットからのアクセス方法について詳しくは、references/typography.mdを参照してください。

ハードコードされたTextStyleの置き換え

サイズと太さを基準に、リテラル値をAppTextStyleで定義済みの最も近いスロットに割り当てます。18px/w500はtitleLarge(20/w500)に、16px/w400はbodyLargeに、14px/w500はlabelLargeに該当します。アプリで異なる値が必要な場合は、そのスロットのサイズをAppTextStyleで調整してください。呼び出し元に数値を残してはいけません。AppTextStyleで定義されておらず、TextThemeにも登録されていないスロットを割り当てないでください。読み取り時にはスタイルが返されますが、Materialのデフォルトのタイポグラフィが使われるため、アプリのフォントが気付かないうちに元に戻ります。呼び出し元でのcopyWithでは、色のロールだけを設定します。

final theme = Theme.of(context);

Text(
  label,
  style: theme.textTheme.titleLarge?.copyWith(
    color: theme.colorScheme.onPrimary,
  ),
)

色だけを入れ替えて、TextStyle(fontSize: 18, fontWeight: FontWeight.w500)をウィジェット内に残しても解決にはなりません。タイポグラフィがテーマの外にある状態は変わりません。

コンポーネントのテーマ

個々のウィジェットインスタンスにスタイルを設定するのではなく、ThemeData の中でコンポーネントのテーマ(例:filledButtonTheme、inputDecorationTheme、appBarTheme)を一元的に定義します。完全な AppTheme クラスでは、ColorScheme、TextTheme、すべてのコンポーネントテーマをまとめて、単一の ThemeData に組み立てます。

FilledButton、InputDecoration、AppBar のテーマ例、テーマ全体の組み立て、ウィジェットからのアクセス方法については、references/components.md を参照してください。

ウィジェットスタイルの重複をなくす

同じ装飾やスタイルが多くのウィジェットインスタンスに現れる場合は、対応するコンポーネントテーマに移し、すべての呼び出し箇所から削除します。ラッパーウィジェット、共通の InputDecoration 定数、buildDecoration() ヘルパーに切り出してはいけません。これらは呼び出し箇所ごとに明示的な適用が必要で、値が ThemeData の外に残り、単純な TextFormField を記述しただけで適用されなくなるためです。

// Right — the defaults live in the theme.
ThemeData(
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    contentPadding: EdgeInsets.symmetric(
      horizontal: AppSpacing.lg,
      vertical: AppSpacing.md,
    ),
  ),
)

各フィールドでは、そのフィールド固有の設定だけを宣言します。

TextFormField(
  decoration: const InputDecoration(labelText: 'Email'),
)

余白システム

基準単位と名前付き定数(xxs から xxlg)を持つ AppSpacing クラスを定義します。各段階は必ず基準単位の倍数とし、独立したリテラル値にはしません。そうすることで初めて、一定の基準を持つスケールになります。

abstract class AppSpacing {
  static const double spaceUnit = 16;

  static const double xxs = 0.25 * spaceUnit; // 4
  static const double sm = 0.5 * spaceUnit; // 8
  static const double md = 0.75 * spaceUnit; // 12
  static const double lg = spaceUnit; // 16
  static const double xxlg = 2 * spaceUnit; // 32
}

スケールを導入するときや、その規約を誰かに伝えるときは、クラスを記述してください。段階名とピクセル値を文章で列挙することは規約ではありません。任意の数値が次に入力されるのを防ぐのは、クラスだからです。

EdgeInsets.only または EdgeInsets.symmetric を使用し、EdgeInsets.fromLTRB は使用しないでください。

完全な AppSpacing クラス、使用例、EdgeInsets の推奨事項については、references/spacing.md を参照してください。

よく使われるパターン

テーマの作成
  1. すべての色定数を含む AppColors を定義します
  2. すべてのテキストスタイル定数を含む AppTextStyle を定義します
  3. 基準単位に基づく余白スケールを持つ AppSpacing を定義します
  4. light と dark のゲッターを持つ AppTheme クラスを作成します
  5. 各 ThemeData で ColorScheme、TextTheme、コンポーネントテーマを設定します
  6. AppTheme.light と AppTheme.dark を MaterialApp に渡します
ウィジェットから明るさの判定を取り除く

明るさによって分岐するウィジェットは、ThemeData に属する判断を担ってしまっています。暗いテーマに対応する新しいウィジェットを追加するたびに分岐が繰り返され、どちらの色もテーマから取得できません。分岐を整理するのではなく、削除してください。ライト用の値とダーク用の値は、2つのテーマで同じ ColorScheme の役割を担います。references/color-scheme.md の例に従って、両方を AppColors に宣言し、それぞれをその役割に割り当て、AppTheme.light と AppTheme.dark を theme と darkTheme として MaterialApp に渡します。すると、ウィジェットでは条件分岐なしで値を1つ取得するだけになります。

@override
Widget build(BuildContext context) {
  return ColoredBox(
    color: Theme.of(context).colorScheme.surface,
    child: child,
  );
}

追加リソース

コメント