Flutter 3.0でスタイル指定をもっと便利にするThemeExtensionの紹介

今年1月からFlutterのWebアプリケーションを開発している孫と申します。

Flutter 3.0から公開されたThemeExtensionはあなたのコードをもっと綺麗に整えるのができます。なぜThemeExtensionシステムを使うべきなのか、どのように使ったらいいのかを確認してみましょう!

Youtubeの「ThemeExtension | Decoding Flutter」の内容を元に編集しました。

WEBではセマンティック情報とスタイル情報を分けた方が良いという話があります。

例えば

<p style="color: red;">どうする?GOする!</p>

上記のように同じところに置くことより

<!-- index.html -->
<p>どうする?GOする!</p>
/* style.css */
p { color: red; }

セマンティックをHTMLファイルへ、スタイルをCSSに置くのがいいという話です。

Text(
    'どうする?GOする!',
    style: TextStyle(color: Colors.red),
)

Flutterで上記のようにText Widgetの文言とスタイルをベタ書きすることができますが、前述したWEBでのセマンティックとスタイルの関係を考えると、スタイルを汎用的に利用できるようにしたほうが良いことがわかります。

Flutterでは、スタイルを汎用的に定義するために、ThemeDataThemeExtensionsを利用する方法があります。

ThemeData

ThemeDataではインスタンスを作ってMaterialAppに指定すると、下位階層のWidgetのデフォルトスタイルになります。

ThemeDataの実装方法を見てみましょう。

MaterialApp(
  theme: ThemeData(
    textTheme: TextTheme(
      bodyText2: TextStyle(
        color: Colors.red
      )
    )
  ),
  home: Text('どうする?GOする!'),
);

上記のようにMaterialAppThemeDataを指定した場合、デフォルトのテキスト色が赤色になり、MaterialAppより下位階層にあるText Widgetのテキスト色は赤色になります。

このように。ThemeDataを定義することで、テキストとスタイル情報を分離することができます。

ThemeDataの課題点

ThemeDataを適用している状態で、一部のUIパーツのスタイルを変更したい時がしばしばあります。

ThemeDataを指定している時、TextButton Widgetに一部カスタムなスタイルを指定することを考えていきましょう。 Flutter 2.0までは、ThemeのtextButtonThemeが持つstyleをコピーし、必要な部分を変更した上で、そのstyleをTextButton Widgetに設定することができました。 例えば、以下のように書くことができます。

TextButton(
    style: Theme.of(
        context,
    ).textButtonTheme.style.copyWith(
        backgroundColor: Colors.red,
    ),
)

個々のUIパーツごとに、styleを毎回指定することのは微妙ですよね。

Flutter 3.0では、上記のことを解決できるThemeExtensionsという新しいシステムがリリースされました。

ThemeExtensions

ThemeExtensionsは、ThemeDataのオブジェクトにカスタムスタイルを追加することができるクラスで、個々のスタイルごとにsubclassを作成して利用することになります。

それでは、ThemeExtensionsを実際に使って見ましょう。

class GoTextButtonStyle extends ThemeExtension<GoTextButtonStyle> {
    const GoTextButtonStyle({
        required this.backgroundColor,
    )};
    
    final Color backgroundColor;
}

backgroundColor持つ、新しいカスタムスタイルを作ります。

ThemeExtensionのsubclassでカスタムスタイルを指定する場合には、以下の2つのメソッドをoverrideする必要があります。

  • copyWith
  • lerp

最初はcopyWithについて解説していきます。

@override
GoTextButtonStyle copyWith({
    Color? backgroundColor,
}) => GoTextButtonStyle(
    backgroundColor: backgroundColor ?? this.backgroundColor,
);

これは、ThemeDataクラスのスタイルと同じく、一般的なcopyの実装をしていきます。

次にlerpについて解説していきます。

@override
GoTextButtonStyle lerp(ThemeExtension<GoTextButtonStyle>? other, double t) {
    if (other is! GoTextButtonStyle) {
        return this;
    }

    return GoTextButtonStyle(
        backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t),
    );
}

複数のテーマが線形補間によって滑らかに変化するためのものです。

カスタムスタイルを作成したので、テーマシステムを拡張して適用していきましょう。

ThemeData(
    extensions: ThemeExtension<dynamic>{
        GoTextButtonStyle(
            backgroundColor: Colors.red,
        ),
    },
)

MaterialAppに指定するThmeDataextensionsGoTextButtonStyleを含めてください。

ThemeExtensionのスタイル作成が完了したので、早速使ってみましょう。

TextButton(
    style: Theme.of(context).extension<GoTextButtonStyle>().style,
)

上記のようにすると既存のTextButtonStyleを利用せず拡張されたGoTextButtonStyleを利用することになります。

上記のようにGoTextButtonStyleを作ったら様々な形のTextButtonを作ることも可能ですが、普段にはColorリストやTextStyleを持つThemeExtensionを作ることが多いかと思います。

綺麗なレイアウト作成に役に立ったら幸いです。