【Flutter】非同期処理中にダイアログを表示する方法

# 目標
Flutterにおける非同期処理の開始前・処理中・完了時にダイアログを表示することが目標です。
- 開始前　→　処理を実行するかを尋ねるダイアログ（Yes or No のダイアログ）
- 処理中　→　`CircularProgressIndicator`や`LinearProgressIndicator`などで処理中であることを知らせるダイアログ
- 完了時　→　処理が完了したことをユーザーに知らせるダイアログ

**※以下では、それぞれを開始前ダイアログ・処理中ダイアログ・完了時ダイアログと表記します。**

# デモンストレーション
大容量ファイルをダウンロードするアプリなどで活用することができます。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745057/e132634d-4809-9418-3a87-649a7ade9002.gif)


# 開発環境
- Windows10 Pro 21H2
- Android Studio Dolphin 2021.3.1
- Android emulator Pixel 4 API 31
- Flutter SDK 2.10.5
- Dart 2.16.2

# 利用したパッケージ・プラグイン
`pubspec.yaml`に追記して、`Pub get`ボタンを押します。
- rxdart 0.27.5
```
dependencies:
  rxdart: ^0.27.5
```

# 処理の流れ
`BLoCデザインパターン`を使います。今回は、以下に示す2つのstreamを用意します。

- Widget → BLoCクラス（ダイアログの操作をBLoCクラスに伝える） `_triggerController`
- BLoCクラス → Widget（表示すべきダイアログの種類をWidgetに伝える） `_dialogController`

1. Widget側でBLoCクラスを初期化し、その直後に`_dialogController`のlistenを開始
2. 開始前ダイアログ表示の合図となるデータ（`Status.inPreparation`）を`_dialogController`に流す
3. 開始前ダイアログ表示
4. 開始前ダイアログで「OK」ボタンが押されたら、`_triggerController`にデータを流す
5. BLoCで非同期処理を開始
6. 同時に処理中ダイアログ表示の合図となるデータ（`Status.inProgress`）を`_dialogController`に流す
7. 処理中ダイアログ表示
8. 非同期処理が終わる
9. 完了時ダイアログ表示の合図となるデータ（`Status.completed`）を`_dialogController`に流す
10. 完了時ダイアログ表示

# 実装
完全版のソースコードはGitHubからご覧になれます。
[リポジトリはこちら](https://github.com/pen-teppo/AsynchronousDialogFlutterApp)
以下で示すコードは重要な部分を抜粋したものです。

### Widget
```dart:main.dart
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:bloc/dialog.dart';
import 'package:bloc/status.dart';

// (中略)

class _MyHomePageState extends State<MyHomePage> {
  late BLoC _BLoC;

  @override
  void initState(){
    super.initState();
    _BLoC = BLoC();
    _BLoC.onDialogChange.listen((Status status) async {
      if(Navigator.of(context).canPop() == true){
        Navigator.pop(context);
      }
      if(status == Status.inPreparation){
        //開始前ダイアログ表示
        bool result = await DialogManager.showNormalDialog(
          context: context,
          title: 'ダウンロードの確認',
          content: 'データをダウンロードしますか？'
        );
        if(result == true){
          _BLoC.triggerAction.add(null);
        }
      }else if(status == Status.inProgress){
        //処理中ダイアログ表示
        await DialogManager.showProgressDialog(
            context: context,
            text: 'ダウンロード中'
        );
      }else if(status == Status.completed){
        //完了時ダイアログ表示
        await DialogManager.showNoticeDialog(
          context: context,
          title: 'ダウンロード完了',
          content: 'ダウンロードが完了しました。'
        );
      }
    });
  }

  @override
  void dispose(){
    _BLoC.close();
    super.dispose();
  }
  // (中略)
}
```
### ダイアログ表示用メソッドを集めたクラス
```dart:dialog.dart
import 'package:flutter/material.dart';

class DialogManager{
  static Future<bool> showProgressDialog({
    required BuildContext context,
    required String text,
  }) async {
    // (中略)
    //ダイアログ表示処理
  }

  static Future<bool> showNormalDialog({
    required BuildContext context,
    required String title,
    required String content
  }) async {
    // (中略)
    //ダイアログ表示処理
  }

  static Future<bool> showNoticeDialog({
    required BuildContext context,
    required String title,
    required String content
  }) async {
    // (中略)
    //ダイアログ表示処理
  }
}
```
### BLoCクラス
```dart:bloc.dart
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:bloc/status.dart';

class BLoC {
  //stream
  final _triggerController = BehaviorSubject<void>();
  Sink<void> get triggerAction => _triggerController.sink;
  
  final _dialogController = BehaviorSubject<Status>();
  Stream<Status> get onDialogChange => _dialogController.stream;

  BLoC(){
    _dialogController.sink.add(Status.inPreparation);

    _triggerController.stream.listen((_) {
      _dialogController.sink.add(Status.inProgress)
      //実際はダウンロード処理を行う
      Future.delayed(Duration(seconds: 3)).then((_) {
        _dialogController.sink.add(Status.completed);
      });
    });
  }

  void close() {
    _triggerController.close();
    _dialogController.close();
  }
}
```
### Status（列挙型）
```dart:status.dart
enum Status{
  inPreparation,
  inProgress,
  completed
}
```
# 課題
ダイアログの切り替え時に`Navigator.pop(context)`を使っています。そのため、処理中に別の画面遷移が割り込んだ場合、想定外の動作になる可能性があります。非同期処理中に、別の画面遷移が割り込まないようにする処理を入れるほうが良いかもしれません。他に問題点がございましたら、ご教授いただければ幸いです。

# 参考文献
本記事を書くにあたり、以下の記事を参考にしました。ありがとうございました。
- yakuran1様
[FlutterBlocでダイアログ表示やtoast表示](https://qiita.com/yakuran1/items/f86b44a94760b327655f)
- tetsufe様
[FlutterでBLoCの値の変化に応じて処理を行う（モーダル表示）](https://qiita.com/tetsufe/items/6529c20d5f8f4a3b4bc0)