[Flutter] v1.22から使用できるボタン系のWidgetと代わりにdeprecatedになるWidgetについて

Flutter v.1.22以降使用可能になるTextButtonなどのWidgetとそれに対応してdeprecatedになるWidgetについて扱います。
2021.02.28

Flutterでよく使われる主要なボタンのいくつかがdeprecatedになって今後新しいボタンに変更する必要があります。

ドキュメントで紹介されている"Old buttons"は、大抵の場合要求する仕様を満たすことができます。一方アプリにカスタムテーマが必要な場合に使いづらい点がありました。Flutterv1.22からOld buttonsを置き換える"new universe of Material buttons"が使用できるようになっています。deprecatedになったボタンはすぐに使用できなくなるわけではなくdeprecatedになった後時間をかけて削除されるそうです。

既存のボタンクラスを改修するのではなく、置き換えるために新しいWidgetとThemeを作成されました。これらを使用することで、アプリやウィジェットレベルでのテーマの定義がかなりシンプルになります。

追加されるWidgetとdeprecatedになるWidgetの対応表が以下です。

https://docs.google.com/document/d/10Fbn59hiHkppqJ6y_1Rjwl7klN-OvJXQU3SFEqicpME/edit#

対応表にある通りTextButtonのThemeはTextButtonThemeという型です。ElevatedButtonとOutlinedButtonに影響を与えずにTextButtonのテキストの色や状態ごとに設定することが可能です。

ButtonThemeという共通の型からButtonごとにThemeが与えられたことによって変更が他のボタンに影響が起きないようになっていますね。

対応表のボタンはどれも使用方法は変わらないので、今回はTextButtonで説明します。

ドキュメントは以下です。

AppBarやダイアログ、その他のコンテンツ内でインラインで使用されることを想定されています。他のコンテンツに紛れて見えるよな箇所での避けるようドキュメントに記載があります。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            TextButton(onPressed: () {}, child: Text('1つ目')),
            TextButton(onPressed: () {}, child: Text('2つ目')),
            TextButton(onPressed: () {}, child: Text('3つ目')),
            TextButton(onPressed: () {}, child: Text('4つ目')),
            TextButton(onPressed: () {}, child: Text('5つ目')),
          ],
        ),
      ),
    );
  }

個別にTextButtonのスタイルを変更するにはTextButtonの引数styleを変更します。アプリ内のTextButton全てのスタイルを変更したい場合は MaterialAppの引き数themeの型ThemeData

のtextButtonThemeをカスタマイズします。

// MaaterialAppをreturnしているWidgeのコード
// ここのテーマ変更はこのアプリ以下のWidget全てに影響する
@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        textButtonTheme: TextButtonThemeData(
            style: TextButton.styleFrom(primary: Colors.red)),
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
}

// MyHomePageのbuildメソッド
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextButton(
                onPressed: () {},
                child: Text('1つ目'),
                style: TextButton.styleFrom(
                  primary: Colors.black,
                )),
            TextButton(onPressed: () {}, child: Text('2つ目')),
            TextButton(onPressed: () {}, child: Text('3つ目')),
            TextButton(onPressed: () {}, child: Text('4つ目')),
            TextButton(onPressed: () {}, child: Text('5つ目')),
          ],
        ),
      ),
    );
  }

次にTextButtonとdeprecatedになるFlatButtonの書き方の違いを見ます。同じようにテキストのサイズと色を変更します。FlatButtonはtextColorにColor型の値を渡します。一方TextButtonはstyleにTextButtonStyle型の値を渡します。styleFromはデフォルトを上書きするButtonStyleを返すのですが、copyWithっぽいですね。

Updating the Material Buttons and their Themes (PUBLICLY SHARED) には原色を指定して ButtonStyle を計算するためのstaticメソッドとしてstyleFrom メソッドが紹介されています。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FlatButton(
              onPressed: () {},
              textColor: Colors.pink,
              child: Text(
                'FlatButton',
                style: TextStyle(fontSize: 30),
              ),
            ),
            TextButton(
              onPressed: () {},
              style: TextButton.styleFrom(
                primary: Colors.pink,
              ),
              child: Text(
                'TextButton',
                style: TextStyle(fontSize: 30),
              ),
            )
          ],
        ),
      ),
    );
  }

ここまで見ると引数で直接色を指定できるFlatButtonの方がシンプルに見えます。次にボタンが選択されている時に色だけを変更してみます。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FlatButton(
              onPressed: () {},
              textColor: Colors.pink,
              child: Text(
                'FlatButton',
                style: TextStyle(fontSize: 30),
              ),
            ),
            TextButton(
              onPressed: () {},
              style: ButtonStyle(foregroundColor:
                  MaterialStateProperty.resolveWith<Color>((states) {
                // ボタンがタップされている時
                if (states.contains(MaterialState.pressed)) {
                  // 緑色
                  return Colors.green;
                }
                // nullを返すとデフォルトに設定されている色になる
                return null;
              })),
              child: Text(
                'TextButton',
                style: TextStyle(fontSize: 30),
              ),
            )
          ],
        ),
      ),
    );
  }

MaterialStatePropertyは列挙型でボタン時の状態に基づいたcaseを持ちます。ボタンの状態に応じてスタイルを変更することができます。今回はタップ時のスタイルを変更するのに使用しました。nullを返すとデフォルトの値が設定されます。

次にMaterialAppの引数themeからアプリ内テーマ全体を変更してみます。先程のMaterialAppのコードをもう一度見ます。

// MaaterialAppをreturnしているWidgeのコード
// ここのテーマ変更はこのアプリ以下のWidget全てに影響する
@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        textButtonTheme: TextButtonThemeData(
            style: TextButton.styleFrom(primary: Colors.red)),
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
}

textButtonThemeをカスタマイズしてFlatButtonとTextButtonのスタイル指定を外します。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FlatButton(
              onPressed: () {},
              child: Text(
                'FlatButton',
                style: TextStyle(fontSize: 30),
              ),
            ),
            TextButton(
              onPressed: () {},
              child: Text(
                'TextButton',
                style: TextStyle(fontSize: 30),
              ),
            )
          ],
        ),
      ),
    );
  }

実行結果は以下です。TextButtonにのみ指定のスタイルが反映されているのが確認できます。

次にtextButtonThemeでなく、これまでButtonのスタイルを変更する時に指定していたbuttonThemeという引数にスタイルを渡してみます。

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        buttonTheme: ButtonThemeData(textTheme: ButtonTextTheme.accent),
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }

これで実行するとFlutButton、TextButtonの両方にスタイルが適応されてしまっているのが確認できます。

ボタンごとに場当たり的にスタイルを渡していくようなシンプルな組み方ではなく、アプリ全体に統一感を持たせるようなスタイルを構成する時にはこれらの新しいButtonはとても便利に感じそうです。buttonThemeにスタイルを指定してしまうと他のボタンのスタイルも変わってしまうので、他のボタンへの影響を考慮する必要がなくなるのがより良いですね。

deprecatedなWidgetからのマイグレーションについてはこの件でdeprecatedなWidgetを移行するためのマイグレーションガイドが提供されています。

まとめ

実はこの件、Twitterで見かけるまで全く知らず、改修の理由もわかってなかったのですがドキュメントを読んで実際に触ってみて納得しました。シンプルなユースケースだと少し書き方が一手間かかる以外は良いアップデートだと思うので個人開発のアプリのボタンも置換していこうと思います。Flutterの学習・開発は始めたばかりで説明やその裏にある認識に誤りがあるかもしれません。なにかお気づきの際はコメントやTwitterなどでお知らせください。