Flutter アプリで使う TextStyle の定義と単純化

https://qiita.com/tokicat/items/bcd64a132897e42a0d82

こちらの記事で、 Material Design Color System や TextTheme に対応したバージョンを公開しました。
この記事のアプローチも有効な場合はあると思いますが、私の環境では新しい方を活用しています。

## 目的
- アプリ内で使う TextStyle を一覧化
- かつ、標準的なスタイル変更を単純に・便利に呼び出せるようにする
    - color
    - fontWeight

### 定義したいスタイル
- [Material Design Typography](https://m3.material.io/styles/typography/overview) にのっとったスタイルのみをアプリ内で使用する
- 用途ごとの定義 (title, body, label...)
- スケールの定義 (large, medium, small)

## 改善前

- 用途 x スケール で定数をふつうに定義すると・・・
    - 定数名が冗長
    - 定義が縦に長くて一覧性が微妙
    - 活用の拡張がめんどい
        - color を変えたい場合など、いちいち `copyWith()` で変更しないといけない

### 活用部分
```dart
  Text(
    "Normal Text!",
    style: MyTextStyles.bodyMedium,
  );

  // MyColors の定義は別に用意があるものとする

  Text(
    "Error!",
    style: MyTextStyles.bodyMedium.copyWith(
      color: MyColors.caution,
    ),
  );

  Text(
    "Large Bold Error!",
    style: MyTextStyles.bodyLarge.copyWith(
      color: MyColors.caution,
      fontWeight: FontWeight.bold,
    ),
  );
```

### 定義部分

```dart
class MyTextStyles {
  static const titleLarge = TextStyle(
    fontFamily: "Noto Sans",
    color: MyColors.black,
    fontSize: 57,
    height: 64 / 57,
    fontWeight: FontWeight.w400,
    letterSpacing: -0.25,
  );
  static const titleMedium = TextStyle(
    fontFamily: "Noto Sans",
    color: MyColors.black,
    fontSize: 45,
    height: 52 / 45,
    fontWeight: FontWeight.w400,
    letterSpacing: 0,
  );
  static const titleSmall = TextStyle(
    fontFamily: "Noto Sans",
    color: MyColors.black,
    fontSize: 36,
    height: 44 / 36,
    fontWeight: FontWeight.w400,
    letterSpacing: 0,
  );

  ...
}
```

## 改善後

### 活用部分
```dart
  Text(
    "Normal Text!",
    style: MyTextStyles.body,
  );

  Text(
    "Error!",
    style: MyTextStyles.body.caution,
  );

  Text(
    "Large Bold Error!",
    style: MyTextStyles.body.large.caution.bold,
  );
```

### 定義部分
```dart
import 'package:flutter/material.dart';

/// Use as:
/// Text("Hello!", styles: MyTextStyles.title.large.white);
class MyTextStyles {
  MyTextStyles._();

  static const fontFamily = "Hiragino Sans";

  static _toStyle(double fontSize, double height, FontWeight fontWeight,
      double letterSpacing,
      {Color color = MyColors.black}) {
    return TextStyle(
      fontFamily: MyTextStyles.fontFamily,
      color: color,
      fontSize: fontSize,
      // baseline 設定がうまくいかず height 指定すると下寄りの文字になってしまうので、適用を保留
      // height: height / fontSize,
      fontWeight: fontWeight,
      letterSpacing: letterSpacing,
    );
  }

  static final title = _MyTextStyleSet(
    large: _toStyle(22, 28, FontWeight.w400, 0),
    medium: _toStyle(16, 24, FontWeight.w500, 0.15),
    small: _toStyle(14, 20, FontWeight.w500, 0.1),
  );

  static final label = _MyTextStyleSet(
    large: _toStyle(14, 20, FontWeight.w500, 0.1),
    medium: _toStyle(12, 16, FontWeight.w500, 0.5),
    small: _toStyle(11, 16, FontWeight.w500, 0.5),
  );

  static final body = _MyTextStyleSet(
    large: _toStyle(16, 24, FontWeight.w400, 0.15),
    medium: _toStyle(14, 20, FontWeight.w400, 0.25),
    small: _toStyle(12, 16, FontWeight.w400, 0.4),
  );

  static final link = _MyTextStyleSet(
    large: _toStyle(16, 24, FontWeight.w400, 0.15, color: MyColors.textlink),
    medium: _toStyle(14, 20, FontWeight.w400, 0.25, color: MyColors.textlink),
    small: _toStyle(12, 16, FontWeight.w400, 0.4, color: MyColors.textlink),
  );
}

/// Wrapper for TextStyle
class _MyTextStyle extends TextStyle {
  _MyTextStyle(TextStyle style)
      : super(
          color: style.color,
          backgroundColor: style.backgroundColor,
          fontSize: style.fontSize,
          fontWeight: style.fontWeight,
          fontStyle: style.fontStyle,
          letterSpacing: style.letterSpacing,
          wordSpacing: style.wordSpacing,
          textBaseline: style.textBaseline,
          height: style.height,
          leadingDistribution: style.leadingDistribution,
          locale: style.locale,
          foreground: style.foreground,
          background: style.background,
          shadows: style.shadows,
          fontFeatures: style.fontFeatures,
          decoration: style.decoration,
          decorationColor: style.decorationColor,
          decorationStyle: style.decorationStyle,
          decorationThickness: style.decorationThickness,
          debugLabel: style.debugLabel,
          overflow: style.overflow,
        );

  _MyTextStyle get black => _MyTextStyle(copyWith(color: MyColors.black));
  _MyTextStyle get white => _MyTextStyle(copyWith(color: MyColors.white));
  _MyTextStyle get caution => _MyTextStyle(copyWith(color: MyColors.caution));

  _MyTextStyle get bold => _MyTextStyle(copyWith(fontWeight: FontWeight.w700));
}

class _MyTextStyleSet extends _MyTextStyle {
  final _MyTextStyle large;
  // medium is this
  final _MyTextStyle small;

  _MyTextStyleSet(
      {required TextStyle large,
      required TextStyle medium,
      required TextStyle small})
      : large = _MyTextStyle(large),
        small = _MyTextStyle(small),
        super(medium);
}
```