FlutterでDialogの表示を変更できなかったお話

今日はFlutterのダイアログのお話。

ダイアログの中にテキストフィールドを準備して、バリデーション違反があったらエラーメッセージを追加したいということがありました。

ダイアログの表示自体はSimpleDialogAlertDialogを使えばできるのですが、表示の変更ができずにハマりました。

以下がコード例。
簡単に、AppBarにあるアイコンをタップするとダイアログが表示されて、ダイアログ内のボタンをタップすると表示をトグルするようなコードにしています。

class _AwesomePageState extends State<AwesomePage> {
  bool _isAwesome;
  String _awesomeText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Everyday Awesome'),
        actions: <Widget>[
          PopupMenuButton(
            onSelected: (String value) {
              showDialog(
                context: context,
                builder: (BuildContext context) => SimpleDialog(
                      title: const Text('It says'),
                      children: <Widget>[
                        const Text('Flutter is awesome.'),
                        RaisedButton(
                          child: const Text('u a Awesome!'),
                          onPressed: () {
                            setState(() {
                              _isAwesome = !_isAwesome;
                              _awesomeText = _isAwesome
                                  ? 'u a Awesome!'
                                  : 'please tap Awesome..';
                            });
                          },
                        ),
                      ],
                    ),
              );
            },
            itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
                  const PopupMenuItem(child: const Text('A'), value: 'a'),
                ],
          )
        ],
      ),
      body: Center(
        child: const Text('Awesome'),
      ),
    );
  }
}

ボタンをタップする度にonPressedの中でsetState()して_awesomeTextが変更されて「u a Awesome!」となるはずですが、何度タップしても変更されません。
ボタンだけでなく、タイトルやテキストを変更しようとしても同じ。
おかしいな、と思ってSimpleDialogのコードを読んでみると、理由がわかりました。

class SimpleDialog extends StatelessWidget {
// ...(略)
}

StatelessWidget!!
setState()しても意味ないはずでした!
念の為AlertDialogDialogクラスを見てみても、どちらも同じくStatelessWidget

ガイドラインをそう読み込んだわけではないのですが、Material Designガイドラインを読むに、ダイアログはあくまでも警告・エラーを表示したり、選択可能なリストを表示したりといったすごくシンプルなUIを目的としているみたい。
じゃあどうすれば…というと、FlutterによるFull-Screen Dialogの実装例があるのを先輩に教えて頂きました。

ちょっと長いですが、要は、StatefulWidgetを作って、SimpleDialogの代わりに呼び出す、ということのよう。
実装するとこんな感じになりました。

class _AwesomePageState extends State<AwesomePage> {
  bool _isAwesome;
  String _awesomeText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Everyday Awesome'),
        actions: <Widget>[
          PopupMenuButton(
            onSelected: (String value) {
              showDialog(
                context: context,
                builder: (_) {
                  return AwesomeDialog();
                },
              );
            },
            itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
                  const PopupMenuItem(child: const Text('A'), value: 'あ'),
                ],
          )
        ],
      ),
      body: Center(
        child: const Text('Awesome'),
      ),
    );
  }
}

class AwesomeDialog extends StatefulWidget {
  @override
  _AwesomeDialogState createState() => _AwesomeDialogState();
}

class _AwesomeDialogState extends State<AwesomeDialog> {
  bool _isAwesome;
  String _awesomeText;

  @override
  void initState() {
    super.initState();
    _isAwesome = false;
    _awesomeText = 'please tap Awesome..';
  }

  Widget _buildAwesomeButton() {
    return RaisedButton(
      child: Text(_awesomeText),
      onPressed: () {
        setState(() {
          _isAwesome = !_isAwesome;
          _awesomeText = _isAwesome ? 'u a Awesome!' : 'please tap Awesome..';
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return SimpleDialog(
      title: const Text('It says'),
      children: <Widget>[
        const Text('Flutter is awesome.'),
        _buildAwesomeButton(),
      ],
    );
  }
}

showDialog関数のbuilderSimpleDialogではなく、AwesomeDialogを渡しています。
AwesomeDialogも返すウィジェットSimpleDialogですが、stateを持ったRaisedButtonを外だしにして渡すことで、stateの変更が反映されるようになりました。

では、最初の例でもRaisedButtonを外だししたり、stateにしてしまえばよいのでは?🤔と思ったけれど、これはうまくいかない。
この辺りはまたいずれ調べてみようと思います!