Widgetに何度も色や大きさを設定するの面倒だな、、、
TextならThemeが使えるけれど、使えないWIdgetはどうすれば良いのかしら?
Widgetの定義と装飾のコードは分けたいわ!
本記事ではこのような悩みにお答えします。
2022年5月に予定されているFlutterアップデートで実装予定の、
自作のWidget
に対してもTheme
にてサイズや色を設定できるようにする、ThemeExtension
について解説します。
Flutterのコードの書き方の常識が次のアップデートで変わるかもしれません。
本記事で是非情報を先取りしてみてください!
課題点と解決方法
課題点
TextWidgetの例
あなたは、Flutterでこんなコードの書き方してはいませんか?
Text(
'sample',
style: TextStyle(fontSize: 24, color: Colors.green),
),
このように、サイズや色などの装飾と、メインとなる内容の部分(上記例だと'sample'
)の部分
を一緒に記載されている方が多いかと思います。
これは、装飾の部分が多くなればなるほど、
縦にコードが長くなり、
内容の部分を探すのに苦労する、
読みにくいコードとなってしまいます。
また、同じ装飾を使い回す、ということを考えたときには、Text Widget
を使うたびに同じ装飾のコードが出てくることとなり、
ボイラープレート(繰り返し)なコードとなってしまいます。
これを解決するのがTheme
です。
MaterialApp
内の、Theme
にて以下のように設定することで、Text
を装飾のコード無しで使用しても、Theme
にて設定した装飾が反映されます。
MaterialApp(
theme: ThemeData(
textTheme: const TextTheme(
bodyText2: TextStyle(fontSize: 24, color: Colors.green),
),
),
home: const MyWidget(),
);
これで、装飾のコードと内容のコードが分離され、
コード量の削減、ボイラープレートなコードの回避が可能となります。
自作Widgetでは、、、?
今の話はFlutterにて用意されているText Widget
についての方法でした。
では自作のWidget
ではどうでしょうか?
例えば、以下のような自作Widgetを考えます。
class Square extends StatelessWidget {
const Square({
Key? key,
this.size = 100,
this.color = Colors.blue,
this.borderRadius = 10,
this.child,
}) : super(key: key);
final double size;
final Color color;
final double borderRadius;
final Widget? child;
@override
Widget build(BuildContext context) {
return Container(
height: size,
width: size,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius),
),
child: child,
);
}
}
このWidget
なら、color
やsize
に初期値が設定されているため、
使用の際の装飾のコードの省略ができ、繰り返しを回避することができそうです。
ただ、内容の実装と装飾の初期値が一緒に定義されているため、
初期値を変えたい時にはこの実装のコードまで見に行かなくてはなりません。
ちょっと面倒ですよね。Text Widget
と装飾のコードの管理が異なるのも管理が複雑になり面倒です。
ただ、自作のWidget
にはTheme
が用意されているわけではないので、
の方法は使えません。Text Widget
どうすれば良いでしょうか?
解決方法
「自作Widget
でも
と同じようにText Widget
Theme
を使いたい」
という課題に対する解決策が、
2022年5月に実施予定のFlutterのアップデートで実装される、【ThemeExtension
】です。
端的に言うと、自作Widget
に対してもTheme
を使えるようにするものとなります。
これにより自作Widget
でも装飾の定義部分と内容の定義部分を
と同じようにわけることができ、Text Widget
よりわかりやすいコードを書くことが可能となります。
具体的な実装例については、次の章から確認していきます。
ThemeExtension の使い方
ThemeExtensionの使い方について解説していきます。
実装の流れは以下の通りです。
ThemeExtension
実装クラスを作成する- 自作
Widget
に1.のクラスを実装する MaterialApp
にて1.のクラスを設定する
一つ一つ解説していきます。
ThemeExtension 継承クラスの作成
まず準備としてText
でのTextStyle
にあたる、ThemeExtension
継承クラスを作成します。
サンプルは以下の通りです。
class SquareStyle extends ThemeExtension<SquareStyle> {
const SquareStyle({
this.color,
this.size,
this.borderRadius,
});
final Color? color;
final double? size;
final double? borderRadius;
//1
@override
ThemeExtension<SquareStyle> copyWith({
Color? color,
double? size,
double? borderRadius,
}) =>
SquareStyle(
color: color ?? this.color,
size: size ?? this.size,
borderRadius: borderRadius ?? this.borderRadius,
);
//2
@override
ThemeExtension<SquareStyle> lerp(
ThemeExtension<SquareStyle>? other, double t) {
if (other is! SquareStyle) {
return this;
}
return SquareStyle(
color: Color.lerp(color, other.color, t),
);
}
}
ThemeExtension
実装クラスでは2つのメソッドのoverride
が必要です。
//1
まず1つ目として、copyWith
メソッドをoverride
する必要があります。
//2
2つ目として、Style
間の変化を定義する、lerp
メソッドをoverride
する必要があります。
自作WidgetへThemeExtension 継承クラスの実装
自作Widget
へ、ThemeExtension
継承クラスを実装します。
Style
クラスの各プロパティを、自作Widget
に反映させるイメージです。
サンプルの実装は以下になります。
class Square extends StatelessWidget {
const Square({
Key? key,
this.style,
this.child,
}) : super(key: key);
final SquareStyle? style;
final Widget? child;
@override
Widget build(BuildContext context) {
//3
final defaultStyle = Theme.of(context).extension<SquareStyle>()!;
final size = style?.size ?? defaultStyle.size;
final color = style?.color ?? defaultStyle.color;
final borderRadius = style?.borderRadius ?? defaultStyle.borderRadius;
return Container(
height: size,
width: size,
decoration: BoxDecoration(
color: color,
borderRadius:
borderRadius != null ? BorderRadius.circular(borderRadius) : null,
),
child: child,
);
}
}
//3BuildContext
から設定されているTheme
を取得しています。
これにより、MaterialApp
で設定したTheme
が反映されます。
MaterialAppへThemeExtension 継承クラスの設定
最後に、MaterialApp
へThemeExtension
継承クラスを設定します。
Text Widget
の時と同じように、ThemeData
のextensions
にて、以下のように設定可能です。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
extensions: const <ThemeExtension<dynamic>>[
SquareStyle(
size: 100,
color: Colors.deepPurple,
),
],
),
home: const MyWidget(),
);
}
}
サンプルコード全体
今回のサンプルコードの全体は以下のとおりです。
import 'package:flutter/material.dart';
const SquareStyle kSquareStyle1 =
SquareStyle(size: 150, color: Colors.green, borderRadius: 10);
const SquareStyle kSquareStyle2 =
SquareStyle(size: 200, color: Colors.red, borderRadius: 50);
void main() {
runApp(const MyApp());
}
class SquareStyle extends ThemeExtension<SquareStyle> {
const SquareStyle({
this.color,
this.size,
this.borderRadius,
});
final Color? color;
final double? size;
final double? borderRadius;
@override
ThemeExtension<SquareStyle> copyWith({
Color? color,
double? size,
double? borderRadius,
}) =>
SquareStyle(
color: color ?? this.color,
size: size ?? this.size,
borderRadius: borderRadius ?? this.borderRadius,
);
@override
ThemeExtension<SquareStyle> lerp(
ThemeExtension<SquareStyle>? other, double t) {
if (other is! SquareStyle) {
return this;
}
return SquareStyle(
color: Color.lerp(color, other.color, t),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(extensions: const <ThemeExtension<dynamic>>[
SquareStyle(
size: 100,
color: Colors.deepPurple,
),
]),
home: const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Square(),
Square(
style: kSquareStyle1,
),
Square(
style: kSquareStyle2,
)
]),
),
);
}
}
class Square extends StatelessWidget {
const Square({
Key? key,
this.style,
this.child,
}) : super(key: key);
final SquareStyle? style;
final Widget? child;
@override
Widget build(BuildContext context) {
final defaultStyle = Theme.of(context).extension<SquareStyle>()!;
final size = style?.size ?? defaultStyle.size;
final color = style?.color ?? defaultStyle.color;
final borderRadius = style?.borderRadius ?? defaultStyle.borderRadius;
return Container(
height: size,
width: size,
decoration: BoxDecoration(
color: color,
borderRadius:
borderRadius != null ? BorderRadius.circular(borderRadius) : null,
),
child: child,
);
}
}
また、以下のDartPadにて今回のコードを紹介しています。
ぜひ併せて確認してみてください。
まとめ
本記事では2022年5月に予定されているFlutterアップデートで実装予定の、
自作のWidget
に対してもTheme
にてサイズや色を設定できるようにする、ThemeExtension
について解説しました。
いかがだったでしょうか?
今回の話はFlutterのコードの書き方を大きく変える、
転換点の話かもしれません。
アップデートが来た際には、ぜひ使ってみてください。
参考
編集後記(2022/5/4)
本記事ではThemeExtension
について解説しました。
個人的な所感をここでは述べさせていただきます。
今まででもText
などでは、Theme
を使った書き方、というのはありました。
これが自作Widget
でもできるようになることで、
コードの書き方のスタンダードが、Theme
を使って装飾と内容を分けるというコード構造に変わるかもしれません。
ディレクトリ構造として、Theme
フォルダを必ず作るようになる、
といった形を想像しています。
一種のHTML/CSSの書き方に近いかもしれません。
もちろん、これに抵抗がある方もいらっしゃるかもしれません。
装飾と内容を分けないコードは、一つにまとまっている、
という点で一定の読みやすさがあるからです。
装飾と内容を分けるか、分けないか、派閥ができるような気がしています。
ただ、どちらでも書ける、というのはFlutterの強みのように思います。
あなたがどんなバックグラウンドを持っていたとしても、
それに近い形でFlutterのコードを書けるからです。
ますますFlutterの将来が楽しみになる、そんな話題だと思いました。
アップデート、楽しみに待ちましょう!
週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。