Riverpodでどこでも使えるLoading Dialog（っぽいもの）を作る


アプリの開発をしていると、非同期の処理をしている間はローディングをしているようなViewを表示して、その他の入力を受け付けないようにしたいケースがあると思います。
androidのProgressDialogの非推奨の理由を見る限り、googleはそういったUXを排除したいようですが、欲しいものは欲しいのでRiverpodと組み合わせてFutureが完了するまで表示されるローディングViewを実装しました。

## 環境
Flutter : 3.3.10-stable
Riverpod : 2.1.1

#### ディレクトリ
```
lib
├── app.dart
├── main.dart　<-- ProviderScopeとMyAppを読み込むだけ
└── loading
    ├── loading.dart
    ├── loading_container.dart
    ├── loading_notifier.dart
    └── loading_screen.dart

```

## loadingの状態を管理するnotifierを作る
bool型のStateNotifierを作成
Futureを受け取り、Future実行前にstateをtrue, 実行後にstateをfalseにするメソッドを定義

```dart:loading_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final loadingProvider = StateNotifierProvider<LoadingNotifier, bool>((ref) => LoadingNotifier());

class LoadingNotifier extends StateNotifier<bool> {
  LoadingNotifier() : super(false);

  Future<T> whileLoading<T>(Future<T> Function() future) {
    return Future.microtask(toLoading)
        .then((_) => future())
        .whenComplete(toComplete);
  }

  void toLoading() => state = true;

  void toComplete() => state = false;
}
```

## loadingのViewを作る
```dart:loading.dart
import 'package:flutter/material.dart';

class Loading extends StatelessWidget {
  const Loading({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      // 背景透過のため
      decoration: const BoxDecoration(color: Color.fromRGBO(0, 0, 0, 0.4)),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // View本体
          Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Center(
              child: CircularProgressIndicator(),
            ),
          ),
        ],
      ),
    );
  }
}
```

Loading ViewをラップするContainerも作る
ここでloadingの状態を受け取り、状態によってLoading Viewの表示を出し分ける

```dart:loading_container.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:progress_dialog_sample/progress/loading_notifier.dart';

import 'loading.dart';

class LoadingContainer extends ConsumerWidget {
  const LoadingContainer({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget? child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final loading = ref.watch(loadingProvider);

    return Stack(
      fit: StackFit.expand,
      children: [
        child ?? const SizedBox(),
        loading ? const Loading() : const SizedBox(),
      ],
    );
  }
}
```

## builderに渡すためのTransitionBuilderを定義
作っておくと他のbuilderに渡すものと共存させやすいので

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

import 'loading_container.dart';

mixin LoadingScreen {
  static TransitionBuilder init({
    TransitionBuilder? builder,
  }) {
    return (BuildContext context, Widget? child) {
      if (builder != null) {
        return builder(context, LoadingContainer(child: child));
      } else {
        return LoadingContainer(child: child);
      }
    };
  }
}
```

## MaterialAppのbuilderに↑を渡す
builderに渡すことで、どの画面でもloading_notifierの状態が変わればローディングのViewを表示させることができる

```dart:app.dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      builder: LoadingScreen.init(),
      home: const LoadingSample(title: 'loading progress sample'),
    );
  }
}
```

## 動かす
ボタンを押すと3秒delayするFutureが動く
Futureが止まるまでLoadingのViewが表示される

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105198/61c44668-e341-4f69-447e-bd43d9eb202e.gif" />