Flutterで遷移した先から前の画面に戻ったときのダイアログを消したい



アプリ開発をしている時、リストのセルをタップして動作確認のダイアログでOKかキャンセルかを表示した後、処理を走らせて終わったら次の画面に遷移させるのってどうやるのかなって思って色々調べたのでそのまとめとして書きました。

## 完成動画
`Dialog`のボタンから処理をして、その後別画面に遷移させようと思った時、ローディング画面がダイアログの背面に移ってしまい、さらに遷移先から戻ってきた時にも、ダイアログが表示されたままになってしまっている。

ダイアログが戻ってきた時にも表示されたままの問題は遷移の方法を`push`から`pushReplace`に変えればいい。

けど、ローディング画面がダイアログの背面に回ってしまうのはどうにかしたい。
これの何が問題かと言うと、ローディング中に何度もダイアログのボタンを押せてしまい、バグの元になってしまう。

<img width="350" alt="AnyConv.com__test1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363545/4347272f-6b10-6fe0-7968-675a81300949.gif">

こっちがそれらの問題を解決した方。

ダイアログのOKを押した後にダイアログを閉じて、その後ローディング。
それが終わると次の画面に遷移する。

これなら、先ほどの問題を解決できる

<img width="350" alt="AnyConv.com__test.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363545/d51d0cc6-836a-490d-71a1-ad5dfc970587.gif">

## 全容

まずこれがサンプルの全体のコード。
やっていることは、リストを表示させてそのセルをタップしたらダイアログを表示。そこで「OK」を押せば`_refresh_`処理が走り、終わったら次のページに遷移。もし、「キャンセル」が押されたら何もせずにダイアログを閉じると言う簡単なもの。

```dart:main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {

  List<int> listItem = [1, 2, 3];
  bool isLoading = false;

  Future<void> _refresh() async {
    // ローディング
    setState(() {
      isLoading = true;
    });

    // 1秒待つ
    await Future.delayed(Duration(seconds: 1));

    // 新しいリストを代入
    setState(() {
      listItem = listItem.map((f) => f + 3).toList();
    });

    // 1秒待つ
    await Future.delayed(Duration(seconds: 1));

    // ロード終わり
    setState(() {
      isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Scaffold(
          appBar: AppBar(title: Text('Dialog'),),
          body: ListView.builder(
            itemBuilder: (context, index) {
              return GestureDetector(
                onTap: () async {
                  final _isRefresh = await showDialog(
                    context: context,
                    builder: (context) {
                      return AlertDialog(
                        title: Text('${listItem[index]}'),
                        actions: <Widget>[
                          FlatButton(
                            child: Text('ok'),
                            onPressed: () => Navigator.of(context).pop(true),
                          ),
                          FlatButton(
                            child: Text('キャンセル'),
                            onPressed: () => Navigator.of(context).pop(false),
                          ),
                        ],
                      );
                    }
                  );
                  if(_isRefresh) {
                    await _refresh();
                    Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
                  }
                },
                child: Card(
                  child: Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Center(child: Text('INDEX = ${listItem[index]}')),
                  ),
                ),
              );
            },
            itemCount: listItem.length,
          ),
        ),
        Visibility(
          visible: isLoading,
          child: DecoratedBox(
            decoration: BoxDecoration(
              color: Color(0x44000000),
            ),
            child: Center(child: const CircularProgressIndicator()),
          ),
        ),
      ],
    );
  }
}

class MySecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Dialog'),),
      body: Center(
        child: Text('Second Screen')
      ),
    );
  }
}

```

## 解説
上から一つずつみていくと

```dart
  Future<void> _refresh() async {
    // ローディング
    setState(() {
      isLoading = true;
    });

    // 1秒待つ
    await Future.delayed(Duration(seconds: 1));

    // 新しいリストを代入
    setState(() {
      listItem = listItem.map((f) => f + 3).toList();
    });

    // 1秒待つ
    await Future.delayed(Duration(seconds: 1));

    // ロード終わり
    setState(() {
      isLoading = false;
    });
  }
```

これは1秒待った後にリストを更新して、さらに1秒待つという関数で、実際の通信とかを再現して作った関数。
ローディングの変数とリストの変数の`setState()`以外特に何もやってない。

次は、`GestureDetector`内の`onTap`の処理部分

```dart
final _isRefresh = await showDialog(
  context: context,
  builder: (context) {
    return AlertDialog(
      title: Text('${listItem[index]}'),
      actions: <Widget>[
        FlatButton(
          child: Text('ok'),
          onPressed: () => Navigator.of(context).pop(true),
        ),
        FlatButton(
          child: Text('キャンセル'),
          onPressed: () => Navigator.of(context).pop(false),
        ),
      ],
    );
  }
);
if(_isRefresh != null && _isRefresh) {
  await _refresh();
  Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
}
```

まず最初に`showDialog`の結果を`_isRefresh`という変数に代入しています。

`showDialog`は`Navigator.of(context).push(...)`と同じように`await`で待つことで遷移先から戻ってきた時の結果などを返してくれる。

この場合だと

```dart
actions: <Widget>[
  FlatButton(
    child: Text('ok'),
    onPressed: () => Navigator.of(context).pop(true),
  ),
  FlatButton(
    child: Text('キャンセル'),
    onPressed: () => Navigator.of(context).pop(false),
  ),
],
```

`Navigator.of(context).pop(true)`と`Navigator.of(context).pop(false)`の部分で`bool`型の値が返され、その結果が`await`して待っている`_isRefresh`変数に代入されるという感じになっています。
※ ダイアログの背景を押して、閉じた場合には何も返さないので注意

こうすることでダイアログでOKが押されたのかキャンセルが押されたのかを判別できるようにして、`_refresh`関数を走らせるかの判定をできるようにしています。

```dart 
if(_isRefresh != null && _isRefresh) {
  await _refresh();
  Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
}
```

ここは前の`showDialog`の部分で`true`が返ってきた(OKが押された)時だけ`_refresh`関数を走らせて、その後次の画面に遷移させるようにしています。

`null`チェックをしているのは、ダイアログの背景を押されたら何も返ってこないため

最後に
これは直接は関係していないですが、ローディングする部分になっています

```dart
Visibility(
  visible: isLoading,
  child: DecoratedBox(
    decoration: BoxDecoration(
      color: Color(0x44000000),
    ),
    child: Center(child: const CircularProgressIndicator()),
  ),
),
```

`Visibility`の`visible`の部分で表示、非表示を切り替えています。
本来の通信ではローディングがいるだろうと思って追加しました。


## 終わりに

Flutterのダイアログの表示方法など最初は慣れるのに苦労したけど、今回の件を通してちょっと分かった気がする。
もっといい方法があるなどがあったらコメントで教えていただけると、すごく助かります