【Flutter】onWillPopでダイアログの閉じを検知する

## 初めに
ダイアログを閉じた際に、何か処理を実行したいときはどうしますか？
`AlertDialog`なら、`action`に設定したボタンタップ時に`Navigator.of(context).pop()`を呼ぶか、`showDialog`の戻り値を受け取る方法をよく見かけます。

Flutterでは、ダイアログの外をタップしても、ダイアログを閉じるように実装されています。
ダイアログの外をタップした時も同様の処理を実行するにはどうすればいいでしょうか？
本記事では、**WiipopScope**を使った方法を紹介します！

https://api.flutter.dev/flutter/widgets/WillPopScope-class.html

`WillPopScope`を使用すると、`Navigator.of(context).pop()`が呼ばれる前の処理を追加することができます。

## WillPopScopeなし
```dart
  void _showFirstDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Hello'),
        content: const Text('閉じるを押さないと、ダイアログの閉じを検知しません。'),
        actions: [
          TextButton(
            onPressed: (() {
              setState(
                () {
                  hasDetectedPop = true;
                },
              );
              Navigator.of(context).pop(true);
            }),
            child: const Text('閉じる'),
          ),
        ],
      ),
    );

    if (result != null && result) {
      setState(
        () {
          hasDetectedPop = true;
        },
      );
    }
  }
```
`閉じる`のボタンでしか検知しない

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2552610/7ea90299-fd1a-aee5-acc6-f9f3b3102a87.gif' width='400'>



## WillPopScopeでDialogをラップ
```dart
  void _showSecondDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => WillPopScope(
        onWillPop: (() {
          Navigator.of(context).pop(true);

          return Future.value(true);
        }),
        child: AlertDialog(
          title: const Text('Hello'),
          content: const Text('ダイアログの外をタップしても、ダイアログの閉じを検知します。'),
          actions: [
            TextButton(
              onPressed: (() {
                Navigator.of(context).pop(true);
              }),
              child: const Text('閉じる'),
            ),
          ],
        ),
      ),
    );
    if (result != null && result) {
      setState(
        () {
          hasDetectedPop = true;
        },
      );
    }
  }
```

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2552610/ca3c317e-7e41-556c-4df2-7870fab1c5cc.gif', width='400'>



<details>

<summary>本記事の全コード</summary>

```dart
import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dialog with WillPopScope Sample',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const DialogPage(),
    );
  }
}

class DialogPage extends StatefulWidget {
  const DialogPage({super.key});

  @override
  State<DialogPage> createState() => _DialogPageState();
}

class _DialogPageState extends State<DialogPage> {
  bool hasDetectedPop = false;

  void _showFirstDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Hello'),
        content: const Text('閉じるを押さないと、ダイアログの閉じを検知しません。'),
        actions: [
          TextButton(
            onPressed: (() {
              setState(
                () {
                  hasDetectedPop = true;
                },
              );
              Navigator.of(context).pop(true);
            }),
            child: const Text('閉じる'),
          ),
        ],
      ),
    );

    if (result != null && result) {
      setState(
        () {
          hasDetectedPop = true;
        },
      );
    }
  }

  void _showSecondDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => WillPopScope(
        onWillPop: (() {
          Navigator.of(context).pop(true);

          return Future.value(true);
        }),
        child: AlertDialog(
          title: const Text('Hello'),
          content: const Text('ダイアログの外をタップしても、ダイアログの閉じを検知します。'),
          actions: [
            TextButton(
              onPressed: (() {
                Navigator.of(context).pop(true);
              }),
              child: const Text('閉じる'),
            ),
          ],
        ),
      ),
    );
    if (result != null && result) {
      setState(
        () {
          hasDetectedPop = true;
        },
      );
    }
  }

  void _showThirdDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => WillPopScope(
        onWillPop: () {
          return Future.value(false);
        },
        child: AlertDialog(
          title: const Text('Hello'),
          content: const Text('閉じるを押さないと、ダイアログは閉じません。'),
          actions: [
            TextButton(
              onPressed: (() {
                setState(
                  () {
                    hasDetectedPop = true;
                  },
                );
                Navigator.of(context).pop(true);
              }),
              child: const Text('閉じる'),
            ),
          ],
        ),
      ),
    );
    if (result != null && result) {
      setState(
        () {
          hasDetectedPop = true;
        },
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            hasDetectedPop
                ? const Text(
                    'ダイアログが閉じられました。',
                    style: TextStyle(
                      color: Colors.red,
                      fontWeight: FontWeight.bold,
                    ),
                  )
                : const Text(
                    'ダイアログは閉じられていません。',
                  ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                TextButton(
                  onPressed: () {
                    _showFirstDialog(context);
                  },
                  child: const Text('FirstDialog'),
                ),
                TextButton(
                  onPressed: () {
                    _showSecondDialog(context);
                  },
                  child: const Text('SecondDialog'),
                ),
                TextButton(
                  onPressed: () {
                    _showThirdDialog(context);
                  },
                  child: const Text('ThirdDialog'),
                ),
              ],
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            hasDetectedPop = false;
          });
        },
        child: const Icon(Icons.refresh),
      ),
    );
  }
}
```
</details>

### 終わりに
本記事では、`WillPopScope`のご紹介をしました。
`Dialogは`、エラー時や、最終確認のために表示することが多いので、使い方をしっかり把握しないといけないですね〜。
`Navigator.of(context).pop()`が呼ばれる前の処理を追加することができるので、他にも、`AppBar`の戻るボタンで画面を戻る前の処理などにも使えそうです。

### 参考

https://minpro.net/flutter-willpopscope