Flutter 拡張しやすいTheme定義について

Flutter 拡張しやすいTheme定義について

ThemeExtensionのススメ
2025.10.27

はじめに

Flutterは基本的にMaterialDesign3に準拠しており、
Theme(ColorSystemTypography)もMaterialDesign3に準ずることを想定した作りになっています。

ですが、ほとんどの場合はMaterialDesign3とは別のデザインシステムで開発を進めることが多いと思います。

そこで本記事では、Flutterのルールに則りつつ、拡張しやすいTheme定義の方法をまとめてみました。

「デザインシステムとは」については、デジタル庁のデザインシステムがとても参考になります。
この辺詳しく教えてくれた先輩には感謝が尽きない。

Themeの構成

ディレクトリ構成

.
└── lib/
    ├── config/
    │   └── theme/
    │       ├── app_colors.dart
    │       ├── app_text_styles.dart
    │       ├── theme_extention.dart
    │       └── theme.dart
    ├── hoge_screen.dart
    └── main.dart

各ファイル

app_colors.dart

ThemeExtensionでカラー拡張を定義しています。
light()のみ用意していますが、ダークモード対応する場合はdark()も定義します。


class AppColors extends ThemeExtension<AppColors> {
  const AppColors({
    required this.hogeRed,
    required this.hogeBlue,
  });

  final Color hogeRed;
  final Color hogeBlue;

  
  AppColors copyWith({
    Color? hogeRed,
    Color? hogeBlue,
  }) {
    return AppColors(
      hogeRed: hogeRed ?? this.hogeRed,
      hogeBlue: hogeBlue ?? this.hogeBlue,
    );
  }

  
  AppColors lerp(AppColors? other, double t) {
    if (other is! AppColors) {
      return this;
    }
    return AppColors(
      hogeRed: Color.lerp(hogeRed, other.hogeRed, t)!,
      hogeBlue: Color.lerp(hogeBlue, other.hogeBlue, t)!,
    );
  }

  static const light = AppColors(
    hogeRed: Color(0xFFFF0000),
    hogeBlue: Color(0xFF0000FF),
  );
}

app_text_styles.dart

ThemeExtensionでテキストスタイル拡張を定義しています。
light()のみ用意していますが、ダークモード対応する場合はdark()も定義します。


class AppTextStyles extends ThemeExtension<AppTextStyles> {
  const AppTextStyles({
    required this.t13NotoSansMedium,
    required this.t15NotoSansBold,
  });

  final TextStyle t13NotoSansMedium;
  final TextStyle t15NotoSansBold;

  
  AppTextStyles copyWith({
    TextStyle? t13NotoSansMedium,
    TextStyle? t15NotoSansBold,
  }) {
    return AppTextStyles(
      t13NotoSansMedium: t13NotoSansMedium ?? this.t13NotoSansMedium,
      t15NotoSansBold: t15NotoSansBold ?? this.t15NotoSansBold,
    );
  }

  
  AppTextStyles lerp(AppTextStyles? other, double t) {
    if (other is! AppTextStyles) {
      return this;
    }
    return AppTextStyles(
      t13NotoSansMedium: TextStyle.lerp(t13NotoSansMedium, other.t13NotoSansMedium, t)!,
      t15NotoSansBold: TextStyle.lerp(t15NotoSansBold, other.t15NotoSansBold, t)!,
    );
  }

  static const light = AppTextStyles(
    t13NotoSansMedium: TextStyle(
      fontFamily: 'NotoSans',
      fontSize: 13,
      fontWeight: FontWeight.w500,
      height: 1,
      letterSpacing: 1,
    ),
    t15NotoSansBold: TextStyle(
      fontFamily: 'NotoSans',
      fontSize: 15,
      fontWeight: FontWeight.w700,
      height: 1,
      letterSpacing: 1,
    ),
  );
}

theme_extention.dart

定義したカラー拡張とテキストスタイル拡張のgetterを定義しています。

extension ThemeDataExtension on ThemeData {
  AppColors get appColors => extension<AppColors>()!;
  AppTextStyles get appTextStyles => extension<AppTextStyles>()!;
}

theme.dart

Themeを定義しています。
ここではわかりやすいようにextensionsのみ指定しています。
lightTheme()のみ用意していますが、ダークモード対応する場合はdarkTheme()も定義します。

ThemeData lightTheme() {
  final theme = ThemeData();
  return theme.copyWith(
    extensions: {
      AppColors.light,
      AppTextStyles.light,
    },
  );
}

main.dart

定義したThemeDataを指定しています。
ダークモード対応する場合は三項演算子でdarkTheme()を指定すればOKです。

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

class App extends StatelessWidget {
  const App({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: lightTheme(),
      home: const HogeScreen(),
    );
  }
}

hoge_screen.dart

Theme.of(context)からThemeDataExtensionで定義したgetterを使ってカラーとテキストスタイルを呼び出し、styleに指定しています。

class HogeScreen extends StatelessWidget {
  const HogeScreen({super.key});

  
  Widget build(BuildContext context) {
    final appColors = Theme.of(context).appColors;
    final appTextStyles = Theme.of(context).appTextStyles;
    return Scaffold(
      body: Text(
        'hoge'
        style: appTextStyles.t13NotoSansMedium.copyWith(
            appColors.hogeRed,
        ),
      );
    );
  }
}

解説

ThemeExtensionを利用することで、Flutterで用意されているThemeの利用方法に則りつつ、管理を一元化しています。

前述のMaterialDesign3ColorSystemTypographyを利用した、ThemeDataのcolorSchemeやtextThemeは非常に便利ですが、
実際キレイにMaterialDesign3の枠に納まることは少ないと思うので、初めからThemeExtensionを用いてるというわけです。

最後に

クラスメソッドのリテールアプリ共創部ではFigmaを使ってアプリのデザイン作成をしています。
デザイナーさんがアプリ実装時のスタイル、コンポーネントの共通化まで考えてデータを作成してくれているおかげで、
フロントエンドの開発も捗り助かっています🙌

デザイン再現はデザイナーへのリスペクトという気持ちでこれからもガシガシ開発していきます。

この記事をシェアする

FacebookHatena blogX

関連記事