Text Widget のスタイリングのためのユーティリティクラス: ThemedText

TextWidget を多用するアプリでは、属性の指定が面倒になりがちです。

https://qiita.com/tokicat/items/124cb02193161f798e53

こちらの記事では、 
Material Design における [Color System](https://m3.material.io/styles/color/the-color-system/key-colors-tones) (Flutter での ColorScheme) と [Custom Colors](https://m3.material.io/styles/color/the-color-system/custom-colors)、
および [TextStyle](https://api.flutter.dev/flutter/material/TextTheme-class.html) を活用することができていませんでした。

この記事は、それらに対応しつつ記述のしやすさを考慮したユーティリティクラスを紹介します。

# 省略したくなる、プレーンな記述

Flutter ColorScheme / CustomColors を活用するには、通常以下のようになります。

```dart
build(BuildContext context) {
  return [
    Text(
      "Color: ColorScheme.primary",
      style: TextStyle(
        color: Theme.of(context).colorScheme.primary,
      ),
    ),
    Text(
      "Color: CustomColors.info",
      style: TextStyle(
        color: Theme.of(context).extension<CustomColors>()!.info,
      ),
    ),
  ];
}
```

さらに TextStyle からフォントのスケール等を指定したり、
[Overflow Ellipsis](https://api.flutter.dev/flutter/widgets/Text-class.html#widgets.Text.1) などを併用すればこうなります。

```dart
build(BuildContext context) {
  return [
    Text(
      "いろいろもりもり",
      style: Theme.of(context).textTheme.labelLarge!.copyWith(
            color: Theme.of(context).colorScheme.primary,
            overflow: TextOverflow.ellipsis,
            fontWeight: FontWeight.bold,
            fontFamily: "NotoSansJP",
          ),
      maxLines: 2,
    ),
  ];
}
```

特段書けないor読めないこともないですが、
私の製作しているアプリでは Text Widget は最も多用する Widget のひとつであったため、
読み書きの感覚を改善したいなと考えました。


# ユーティリティを活用した省略結果

そこで、以下のようにシンプルに書けるようなユーティリティクラスを定義しました。

```dart
build(BuildContext context) {
  return [
    ThemedText(context)
        .labelLarge
        .primary
        .overflowEllipsis
        .bold
        .notoSansJP
        .maxLines(2)
        .build("いろいろもりもり　改善後"),
  ];
}
```

# ユーティリティクラス: ThemedText

私の環境ではこのようなメソッド群を定義していますが、
用途に合わせて整備するのがよいかと思います。

```dart

/// BuildContext に基づいたテーマを便利に取得して割り当て、Text Widget に割り当てて返すシンタックスシュガーよりのユーティリティ
///
/// - [TextTheme] の提供する各種 [TextStyle]
/// - [ColorScheme] の提供する各種 [Color]
/// - [CustomColor] の提供する各種 [Color]
///
/// (ex) ThemedText(context).displayLarge.success.build("Hello World!");
class ThemedText {
  ThemedText(BuildContext context) : _context = context;

  final BuildContext _context;
  late final TextTheme _textTheme = Theme.of(_context).textTheme;
  late final ColorScheme _colorScheme = Theme.of(_context).colorScheme;
  late final CustomColors _customColors =
      Theme.of(_context).extension<CustomColors>()!;
  late TextStyle _textStyle = _textTheme.bodyMedium!;
  String? _fontFamily;
  double? _fontSize;
  Color? _color;
  FontWeight? _fontWeight;
  int? _maxLines;
  TextOverflow? _overflow;
  TextHeightBehavior? _textHeightBehavior;

  // Fonts
  ThemedText _setFontFamily(String fontFamily) {
    _fontFamily = fontFamily;
    return this;
  }

  ThemedText get notoSansJP => _setFontFamily("NotoSansJP");
  ThemedText get roboto => _setFontFamily("Roboto");

  // TextStyles

  ThemedText _setTextStyle(TextStyle? textStyle) {
    if (textStyle == null) {
      throw AppException.error("textStyle is null");
    }
    _textStyle = textStyle;
    return this;
  }

  ThemedText get displayLarge => _setTextStyle(_textTheme.displayLarge);
  ThemedText get displayMedium => _setTextStyle(_textTheme.displayMedium);
  ThemedText get displaySmall => _setTextStyle(_textTheme.displaySmall);
  ThemedText get headlineLarge => _setTextStyle(_textTheme.headlineLarge);
  ThemedText get headlineMedium => _setTextStyle(_textTheme.headlineMedium);
  ThemedText get headlineSmall => _setTextStyle(_textTheme.headlineSmall);
  ThemedText get titleLarge => _setTextStyle(_textTheme.titleLarge);
  ThemedText get titleMedium => _setTextStyle(_textTheme.titleMedium);
  ThemedText get titleSmall => _setTextStyle(_textTheme.titleSmall);
  ThemedText get bodyLarge => _setTextStyle(_textTheme.bodyLarge);
  ThemedText get bodyMedium => _setTextStyle(_textTheme.bodyMedium);
  ThemedText get bodySmall => _setTextStyle(_textTheme.bodySmall);
  ThemedText get labelLarge => _setTextStyle(_textTheme.labelLarge);
  ThemedText get labelMedium => _setTextStyle(_textTheme.labelMedium);
  ThemedText get labelSmall => _setTextStyle(_textTheme.labelSmall);

  // Colors

  ThemedText _setColor(Color? color) {
    if (color == null) {
      throw AppException.error("color is null");
    }
    _color = color;
    return this;
  }

  ThemedText get primary => _setColor(_colorScheme.primary);
  ThemedText get onPrimary => _setColor(_colorScheme.onPrimary);
  ThemedText get primaryContainer => _setColor(_colorScheme.primaryContainer);
  ThemedText get onPrimaryContainer =>
      _setColor(_colorScheme.onPrimaryContainer);
  ThemedText get secondaryContainer =>
      _setColor(_colorScheme.secondaryContainer);
  ThemedText get onSecondaryContainer =>
      _setColor(_colorScheme.onSecondaryContainer);
  ThemedText get tertiary => _setColor(_colorScheme.tertiary);
  ThemedText get onTertiary => _setColor(_colorScheme.onTertiary);
  ThemedText get tertiaryContainer => _setColor(_colorScheme.tertiaryContainer);
  ThemedText get onTertiaryContainer =>
      _setColor(_colorScheme.onTertiaryContainer);
  ThemedText get errorContainer => _setColor(_colorScheme.errorContainer);
  ThemedText get onErrorContainer => _setColor(_colorScheme.onErrorContainer);
  ThemedText get surfaceVariant => _setColor(_colorScheme.surfaceVariant);
  ThemedText get onSurfaceVariant => _setColor(_colorScheme.onSurfaceVariant);
  ThemedText get outline => _setColor(_colorScheme.outline);
  ThemedText get outlineVariant => _setColor(_colorScheme.outlineVariant);
  ThemedText get shadow => _setColor(_colorScheme.shadow);
  ThemedText get scrim => _setColor(_colorScheme.scrim);
  ThemedText get inverseSurface => _setColor(_colorScheme.inverseSurface);
  ThemedText get onInverseSurface => _setColor(_colorScheme.onInverseSurface);
  ThemedText get inversePrimary => _setColor(_colorScheme.inversePrimary);
  ThemedText get surfaceTint => _setColor(_colorScheme.surfaceTint);
  ThemedText get onSurface => _setColor(_colorScheme.onSurface);
  ThemedText get error => _setColor(_colorScheme.error);
  ThemedText get onError => _setColor(_colorScheme.onError);

  // CustomColors

  ThemedText get customSourceSuccess => _setColor(_customColors.sourceSuccess);
  ThemedText get customSuccess => _setColor(_customColors.success);
  ThemedText get customOnSuccess => _setColor(_customColors.onSuccess);
  ThemedText get customSuccessContainer =>
      _setColor(_customColors.successContainer);
  ThemedText get customOnSuccessContainer =>
      _setColor(_customColors.onSuccessContainer);
  ThemedText get customSourceInfo => _setColor(_customColors.sourceInfo);
  ThemedText get customInfo => _setColor(_customColors.info);
  ThemedText get customOnInfo => _setColor(_customColors.onInfo);
  ThemedText get customInfoContainer => _setColor(_customColors.infoContainer);
  ThemedText get customOnInfoContainer =>
      _setColor(_customColors.onInfoContainer);

  // Other Custom

  ThemedText fontSize(double fontSize) {
    _fontSize = fontSize;
    return this;
  }

  ThemedText fontWeight(FontWeight fontWeight) {
    _fontWeight = fontWeight;
    return this;
  }

  ThemedText get bold {
    _fontWeight = FontWeight.bold;
    return this;
  }

  ThemedText maxLines(int maxLines) {
    _maxLines = maxLines;
    return this;
  }

  ThemedText get overflowEllipsis {
    _overflow = TextOverflow.ellipsis;
    return this;
  }

  ThemedText textHeightBehavior(TextHeightBehavior textHeightBehavior) {
    _textHeightBehavior = textHeightBehavior;
    return this;
  }

  TextStyle get style => _textStyle.copyWith(
        color: _color,
        fontFamily: _fontFamily,
        fontSize: _fontSize,
        fontWeight: _fontWeight,
      );

  Text build(String text) {
    return Text(
      text,
      maxLines: _maxLines,
      overflow: _overflow,
      textHeightBehavior: _textHeightBehavior,
      style: style,
    );
  }
}
```