flutter1.22から追加されたMaterialStateをサクッと解説する
はじめに
flutter1.22からボタンに関して変更がありました。
前のWidget | 新しいWidget |
---|---|
FlatButton | TextButton |
OutlineButton | OutlinedButton |
RaisedButton | ElevatedButton |
上の表のように代わりとなるボタンAPIがいくつか用意されましたが、これに伴ってボタンのスタイル定義がMaterialStatePropertyを軸にしたものに変わりました。
MaterialStatePropertyはボタンに限らずあらゆるマテリアルウィジェットにスタイルを定義する上での共通インターフェースとなるようなので、今後大事なものになるかと思われます。
環境
バージョン | |
---|---|
flutterSDK | 1.22.3 |
DartSDK | 2.10.3 |
本題
今回は基本的にボタンを例にとって話を進めていきます。
以前のスタイル定義から変わった点
以前のスタイル定義はボタンにて直接定義するほか、MaterialAppに渡すThemeとして定義する2つの方法がありました。
RaisedButton(
// ...
color: Colors.red,
disabledColor: Colors.grey
// ...
)
ButtonThemeData(
// ...
buttonColor: Colors.red,
disabledColor: Colors.grey
// ...
)
コミュニティの動きを追っているわけではないので詳しいことはわかりかねますが、スタイル定義のためのインターフェースが統一されていないことが問題視されていたのではないでしょうか?
新しいMaterialStatePropertyはボタンのみならず、多くのマテリアルウィジェットのスタイル定義に一貫性をもたらしてくれるようです。
例えばボタンのスタイル定義は新しく追加されたButtoStyleクラスで行われます。このクラスはElevatedButton
などの新しく追加されたウィジェットに指定できる他、themeにおける各種buttonThemeの指定方法もこれになっています。
ButtonStyle({
MaterialStateProperty<TextStyle> textStyle,
MaterialStateProperty<Color> backgroundColor,
MaterialStateProperty<Color> foregroundColor,
MaterialStateProperty<Color> overlayColor,
MaterialStateProperty<Color> shadowColor,
MaterialStateProperty<double> elevation,
MaterialStateProperty<EdgeInsetsGeometry> padding,
MaterialStateProperty<Size> minimumSize,
MaterialStateProperty<BorderSide> side,
MaterialStateProperty<OutlinedBorder> shape,
MaterialStateProperty<MouseCursor> mouseCursor,
VisualDensity visualDensity,
MaterialTapTargetSize tapTargetSize,
Duration animationDuration,
bool enableFeedback,
}
ご覧のようにほとんどのプロパティがMaterialStatePropertyになっています。
MaterialStateとは
MaterialStatePropertyはMaterialStateと一緒に使うAPIです。
MaterialStateとはマテリアルデザインコンポーネントにおいて、ユーザーの入力を受け取ったコンポーネントが取る状態のことです。例としてボタンが押されたときのpressed
、入力不可にしたいときのdisabled
などがあります。Flutterではenumの形式になっています。
公式のマテリアルデザインガイドに詳しい説明が乗っているので、気になった方は読んでみてください。
Set
基本的にすべてのウィジェットはMaterialStateをSetの形で受け取ります。Setとは簡単に言うと内部の要素の重複を許さない配列のことです。
MaterialStateは以下のように複数の状態を同時に取るという状況が起こりえます。
[
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
]
しかし同じ状態を複数保つ意味がないのでListなどの重複があるものではなく、Setを使用しているのだと思われます。
MaterialStateProperty
基本的な使い方
MaterialStatePropertyは簡単に言うとSet<MaterialState>
を受け取り、それに対応するスタイルを返すためのインターフェースです。ここで言うスタイルとはColor
やBorder
、EdgeInsetsGeometry
などのクラスのことです。例えばpressed
のときはColors.blue
を返し、disabled
のときはColors.grey
を返すといった具合にウィジェットの状態に合わせてスタイルを返すための仕組みです。
大抵のthemeData系クラスはMaterialStateProperty<T>
の形でスタイルを受け取る形になっており、MaterialPropertyクラスのstaticメソッドであるall
とresolveWith
を使って指定していくのが基本になります。
MaterialStateProperty.all
こちらは受け取るMaterialStateに関わらず、すべての状態において同じスタイルを定義したいときに使います。例えばボタンのテキストカラーを状態に関わらず常に白に保っておきたいときなどに以下のように使います。
ElevatedButton(
onPressed: () {},
child: Text('Button'),
style: ButtonStyle(
textStyle: MaterialStateProperty.all(
const TextStyle(color: Colors.white),
),
),
),
MaterialStateProperty.resolveWith
こちらは受け取ったMaterialStateによって、スタイルを出し分けたいときに使用するメソッドになります。引数にMaterialPropertyResolver<T>
という型を持つコールバックを取ります。これは引数にSet<MaterialState>
を取り、Tを返すという関数の形になります。例えばボタンの背景色を変えたい場合は以下のように、Color
を返すコールバックを定義し、backgroundColor
に割り当てます。
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Colors.blue;
}
return Colors.red;
}
// ...
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith(getColor),
),
)
// ...
コールバック内で現在のstateをany
などのテスト関数にかけて条件分岐を行い、対応するスタイルを返しています。基本的にはこういうシンプルな使い方になるかと思われます。
インタラクティブな例もあったほうが良いかと思ったので、簡単なものですがcodepenを用意しました。
他にもresolveAs
というメソッドがあるようですが、こちらはMaterialProperty<T>
とT
のいずれかを受け取るようなプロパティを指定する際に使用するメソッドのようです。僕には使い所が思いつかなかったので今回は省略します。
おまけ:Themeで指定した基底パラメータのオーバーライド
書くまでもないかもしれませんが、Theme内で指定した各種buttonThemeをオーバーライドして一部だけ変えたい場合は以下のように書くと適用されます。オーバーライドのときもMaterialStatePropertyを返す必要があるので、少々記述量は増えたかな?
ElevatedButton(
onPressed: () {},
child: Text('Hoge'),
style: Theme.of(context).elevatedButtonTheme.style.copyWith(
textStyle: MaterialStateProperty.all(
Theme.of(context).textTheme.bodyText1,
),
),
),
まとめ
今回の記事を短くまとめるとこんな感じです。
- MaterialStateはマテリアルウィジェットがユーザー入力によって受け取る状態
- MaterialStatePropertyを使ってElevatedButtonなどの新しいマテリアルウィジェットのスタイル定義が可能
- MaterialStateProperty.allはstateに関わらず、常に同じスタイルを返すためのメソッド
- MaterialStateProperty.resolveWithはMaterialStateのSetを受け取り、ウィジェットの状態によって返すスタイルを分けることができるメソッド
おそらく今後スタイルの定義はこのMaterialStateを用いたものにどんどん変わっていくと思いますが、ルールが単純明快な上に表現できるスタイルが幅広いというかなりいい仕組みだと思うのでぜひ使ってみてください。
Discussion
ボタンのstyle指定では、
ボタン名.styleFrom()
を通常もっとも多用するはずです。これがあるので、
MaterialStateProperty.all
はあまり使わず、そのコード例は以下のようにより簡潔に書けます。styleFrom method - ElevatedButton class - material library - Dart API
このメソッドイマイチ使い方がわかっていなかったので、コメントすごく助かります!stateによって出し分けたいときだけ、MaterialStatePropertyクラスに触れる必要がある感じなんですね。