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>を受け取り、それに対応するスタイルを返すためのインターフェースです。ここで言うスタイルとはColorBorderEdgeInsetsGeometryなどのクラスのことです。例えばpressedのときはColors.blueを返し、disabledのときはColors.greyを返すといった具合にウィジェットの状態に合わせてスタイルを返すための仕組みです。

大抵のthemeData系クラスはMaterialStateProperty<T>の形でスタイルを受け取る形になっており、MaterialPropertyクラスのstaticメソッドであるallresolveWithを使って指定していくのが基本になります。

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を用いたものにどんどん変わっていくと思いますが、ルールが単純明快な上に表現できるスタイルが幅広いというかなりいい仕組みだと思うのでぜひ使ってみてください。