【Flutter】自動生成を駆使してデザインやテーマをまとめて管理できるパッケージを作った

# Katana Theme

こんにちは。広瀬マサルです。

今回はテーマやデザイン周りの整備を行いました。

[flutter_gen](https://pub.dev/packages/flutter_gen)のようなアセットやフォントの自動生成を行ないつつ

**色やテキストスタイルなどを体系的に管理可能なパッケージ**

を作成しました。

使い方をまとめたので興味ある方はぜひ使ってみてください！

# katana_theme

https://pub.dev/packages/katana_theme

https://pub.dev/packages/katana_theme_builder

# はじめに

Flutterのテーマの扱いは複雑怪奇です。

Flutterでは`ThemeData`にアプリのテーマがまとめられていますが、ThemeData内に様々なテーマが入っており、どのテーマがそもそも存在するのか、どのテーマを変更すればウィジェット内のどこが変わるのか非常に分かりづらいです。

Flutterは基本的にMaterialDesignをベースにしています。

そのためMaterialDesign用のカラースキームやテキストサイズを用いてデザインをシンプルに定義できるようなパッケージを作成しました。

合わせて[flutter_gen](https://pub.dev/packages/flutter_gen)などで使用されている`assets`や`fonts`のファイル構成からコードを自動生成する仕組みを導入し体系的に管理できるようにしています。

下記の機能が利用可能です。

- MaterialDesing3のColorSchemeで色を指定
- MaterialDesign3のTypeScaleで文字サイズを指定
- 上記をThemeDataに適用してアプリに指定できる機能
- assetsフォルダから画像へのパスおよびImageProviderを取得できるコードの自動生成
- fontsフォルダからフォントファミリーの指定が可能なコードの自動生成
- テーマからのグラデーションカラーの指定
- その他簡易的なデザイン変換用メソッド

下記のように記載可能です

```dart
@appTheme
final theme = AppTheme(
  primary: Colors.blue,
  secondary: Colors.cyan,
);

Text(
  "test text",
  style: theme.text.bodyMedium.withColor(theme.color.primary),
)
```

# インストール

[build_runner](https://pub.dev/packages/build_runner)を用いたコードジェネレーションを行うため下記のパッケージをインポートします。

```bash
flutter pub add katana_theme
flutter pub add --dev build_runner
flutter pub add --dev katana_theme_builder
```

# 実装

## テーマ作成

まず`@appTheme`のアノテーションをつけて、`AppThemeData`を作成します。

@appThemeを利用するファイルには`part ‘元のファイル名.theme.dart’`を追加してください。

```dart
// theme.dart

part 'theme.theme.dart';

@appTheme
final theme = AppThemeData();
```

## AppThemeScopeの作成とMaterialAppへのテーマ適用

`MaterialApp`などの上に`AppThemeScope`を作成し、先程定義した`AppThemeData`を渡します。

MaterialAppのthemeには`theme.toThemeData()`を渡すことでアプリ内にAppThemeData内で定義したテーマを適用することができます。

```dart
// main.dart

AppThemeScope(
  theme: theme,
  child: MaterialApp(
    home: const MyHomePage(
      title: "Flutter Demo",
    ),
    title: "Flutter Demo",
    theme: theme.toThemeData(),
  ),
);
```

## アセットの定義

下記の公式サイトを参考にpubspec.yamlを編集し、アプリ内でアセットを読み込めるようにしてください。

https://docs.flutter.dev/development/ui/assets-and-images

```yaml
// pubspec.yaml

flutter:
  assets:
    - assets/images/
```

## フォントの定義

下記の公式サイトを参考にpubspec.yamlを編集し、アプリ内でフォントを読み込めるようにしてください。

https://docs.flutter.dev/cookbook/design/fonts

```yaml
// pubspec.yaml

flutter:
  fonts:
    - family: RobotoMono
      fonts:
        - asset: fonts/RobotoMono-Regular.ttf
        - asset: fonts/RobotoMono-Bold.ttf
          weight: 700
```

# コードジェネレーション

下記のコマンドを入力することで自動でコード生成を行います。

```bash
flutter pub run build_runner build --delete-conflicting-outputs
```

# テーマの指定

`AppThemeData`にテーマを指定することができます。

マテリアルデザインのカラースキームに従い下記の色を指定可能です。

https://m3.material.io/styles/color/the-color-system/key-colors-tones

- primary
- secondary
- tertiary
- primaryContainer
- secondaryContainer
- tertiaryContainer
- disabled
- outline
- error
- surface
- background
- onPrimary
- onSecondary
- onTertiary
- onPrimaryContainer
- onSecondaryContainer
- onTertiaryContainer
- onDisabled
- onSurface
- onBackground
- onError

さらに追加色として下記を設定可能です。

- weak
    - 薄字の色。あまり目立たせたくない色に使用
- warning
    - 注意の色。
- info
    - ちょっとした情報を表示する際の色
- success
    - 成功時の色。
- onWeak
    - `weak`を背景色にしたときの文字色。
- onInfo
    - `info`を背景色にしたときの文字色。
- onSuccess
    - `success`を背景色にしたときの文字色。
- onWarning
    - `warning`を背景色にしたときの文字色。
- splashColor
    - ボタンをタップしたときのエフェクトカラー。
- shadow
    - 影の色。
- inverseSurface
    - `surface`の反転色。
- onInverseSurface
    - `inverseSurface`を背景色にしたときの文字色。

マテリアルデザインのTypographyに従い下記のTypeScaleを指定可能です。

- displayLarge
- displayMedium
- displaySmall
- headlineLarge
- headlineMedium
- headlineSmall
- titleLarge
- titleMedium
- titleSmall
- bodyLarge
- bodyMedium
- bodySmall
- labelLarge
- labelMedium
- labelSmall

# 使い方

`AppThemeData`はグローバルで定義されているのでどこからでも参照可能です。

`AppThemeData`の中からは下記のテーマが取得可能です。

- `color`
    - `AppThemeData`作成時に定義したColorScheme。
- `text`
    - `AppThemeData`作成時に定義したTypeScale
- `asset`
    - コードジェネレーションで作成した`assets`フォルダ以下のアセット
- `font`
    - コードジェネレーションで作成したFontFamily。
- `widget`
    - インジケーターなどの固定ウェジェット定義。
    - 後述の`テーマ拡張`で定義します。

```dart
class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Title"), backgroundColor: theme.color.secondary),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children:[
          Center(child: CircleAvatar(backgroundImage: theme.asset.userIcon.provider)),
          Text("User Name", style: theme.text.displayMedium)
        ]
      )
    );
  }
}
```

# テーマ拡張

アプリによっては特殊な色や文字サイズを使う場合もあります。

1箇所でよければ直接ウィジェットに指定してもよいのですが、アプリ内の複数箇所にまたがる場合はテーマで指定したほうが効率的です。

このパッケージでは`extension`を用いることにより新しいテーマを追加します。

下記のクラスを`on`に指定して`extension`を作成可能です。

- `ColorThemeData`
    - 色を追加します。
- `TextThemeData`
    - 文字サイズなどのTextStyleを追加します。
- `AssetThemeData`
    - 画像などのアセットを追加します。
- `WidgetThemeData`
    - インジケーターなどのウェジェットを追加します。

```dart
extension ColorThemeDataExtensions on ColorThemeData {
  Color get myColor => Colors.red;
}

extension WidgetThemeDataExtensions on WidgetThemeData {
  Widget get indicator => const LinearProgressIndicator();
}
```

# 応用した使い方

## グラデーション

テーマに`GradientColor`を指定することでグラデーションを適用することが可能です。

`GradientColor`はなにもしないと通常色として認識されますが、`toLinearGradient()`でグラデーションカラーとして利用可能です。

```dart
// theme.dart

@appTheme
final theme = AppThemeData(
  primary: GradientColor(
    Colors.red,  // こちらが通常色として認識される
    Colors.white,
  )
);

// test_page.dart

class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Title")),
      body: Container(
        decoration: BoxDecoration(
          gradient: theme.color.primary.toLinearGradient(),
        ),
      )
    );
  }
}
```

## 変換用メソッド

実際に使用する際にテーマのデータに若干修正を加えて出力可能です。

```dart
final Color darkenColor = theme.color.primary.darken(); // すこし色を暗くして出力
final Color lightenColor = theme.color.primary.lighten(); // すこし色を明るくして出力

final TextStyle smallizeText = theme.text.bodyMedium.smallize(); // すこし文字サイズを小さくして出力
final TextStyle largizeText = theme.text.bodyMedium.largize(); // すこし文字サイズを大きくして出力
final TextStyle changedFontSizeText = theme.text.bodyMedium.withSize(18); // フォントサイズを18にして出力

final TextStyle darkenText = theme.text.bodyMedium.darken(); // すこし文字色を暗くしてして出力
final TextStyle lightenText = theme.text.bodyMedium.lighten(); // すこし文字色を明るくして出力
final TextStyle changedFontColorText = theme.text.bodyMedium.withColor(theme.color.primary); // すこし文字色をプライマリーカラーにして出力

final TextStyle changedOpacityText = theme.text.bodyMedium.withOpacity(0.5); // 文字の透明度を半分にして出力
final TextStyle changedFontWeightText = theme.text.bodyMedium.withBold(); // 文字を太字にして出力
```

# おわりに

自分で使う用途で作ったものですが実装の思想的に合ってそうならぜひぜひ使ってみてください！

また、[こちら](https://github.com/mathrunet/flutter_masamune)にソースを公開しているのでissueやPullRequestをお待ちしてます！

また仕事の依頼等ございましたら、私の[Twitter](https://twitter.com/mathru)や[Webサイト](https://mathru.net)で直接ご連絡をお願いいたします！

https://mathru.net/ja/contact

# GitHub Sponsors

スポンサーを随時募集してます。ご支援お待ちしております！

https://github.com/sponsors/mathrunet