🐥

Riverpod で安全な Loading ダイアログ試作

2022/10/10に公開
1

課題感

こういう実装が多い


class HogePage extends StatelessWidget {

// ~~
    onPressed: () async {
	showDialog(context: context, (_) => Dialog());
	await doAysnc();
	// ここ
	Navigator.of(context).pop();
    }
}

何を pop() してるか分からなかったり(まあコメントつければ良いが)、もし何かのミスでダイアログが閉じられてしまった or 表示されなかった。など変な挙動になってしまう。
というか、変な挙動になりうるの実装なのが嫌だ。

実装内容

ref.listen を活用して、StateProvider で表示/非表示を管理する。
ref.listen の一般的な活用法。

ref.listen: プロバイダの値を監視し、値が変化するたびに呼び出されるコールバック関数(画面遷移、ダイアログの表示など)を登録する。
引用元:https://riverpod.dev/ja/docs/concepts/reading/#ref-を使ってプロバイダを利用する

final _isLoading = StateProvider((_) => false);

/// ローディングダイアログの表示
final showLoadingDialog = Provider<VoidCallback>((ref) {
  return () {
    ref.read(_isLoading.notifier).update((_) => true);
    unawaited(showDialog(
        context: ref.read(navigatorKeyProvider).currentContext!,
        barrierDismissible: false,
        builder: (_) => const LoadingDialog()));
  };
});

/// ローディングダイアログの表示
final hideLoadingDialog = Provider<VoidCallback>((ref) {
  return () {
    // State の更新のみ
    ref.read(_isLoading.notifier).update((_) => false);
  };
});

class LoadingDialog extends ConsumerStatefulWidget {
  const LoadingDialog({Key? key}) : super(key: key);

  
  ConsumerState<ConsumerStatefulWidget> createState() => _LoadingDialogState();
}

class _LoadingDialogState extends ConsumerState<LoadingDialog> {

  
  Widget build(BuildContext context,) {
        // state の更新を検知して、pop させる
        ref.listen<bool>(_isLoading, ((_, next) {
          if (mounted) {
	Navigator.pop(context);
      }
    }));

    return AlertDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          SizedBox(
            height: 50,
            width: 50,
            child: CircularProgressIndicator(),
          ),
          const SizedBox(
            height: 32,
          ),
          const Text('ローティングなう'),
        ],
      ),
      contentPadding: const EdgeInsets.all(32),
    );
  }
}

問題点?

  • ダイアログが複数出ていた場合は、変な挙動をするが、そもそもそんな状況はないので、考えなくて良い。
  • Riverpod などのグローバルな状態管理パッケージに依存しているが、状態管理を StatefulWidget などのみに依存することはないのでとりあえず大丈夫。
    • Stack を活用した実装をしてくれた方がいたので紹介しておく。この方法なら、Widget で囲う必要があるが、複数のダイアログが出てしまった場合などは考えなくて良くなる。
      https://zenn.dev/takewoy/articles/7492ffebc62414

利点

  • _isLoading の StateProvider はプライベートで、「呼び出す」と「非表示にする」のみを公開しているので安全。
  • どういう場合でも「呼び出す」と「非表示にする」を呼び出せば実現できるので、良い感じに隠蔽化されている。

発展

hideLoadingDialog の機能拡張。ダイアログを非表示にするときに、メッセージを渡すことで、SnackBar を表示させている。
SnackBar でなくとも、いろんな拡張できる。
ダイアログを非表示にさせる以上のことをしているので、かならずしもベストな実装とは言えないが、引数は optional だし、「ローディンングが終わった後に、何かしらメッセージを表示する」は一般的な実装なので、個人開発とかではガンガン使う気がする。

/// errorMessage / successMessage を渡すことで、snackBar を表示させられる。
final hideLoadingDialog =
    Provider<void Function({String? errorMessage, String? successMessage})>(
        (ref) {
  return ({String? errorMessage, String? successMessage}) {
    ref.read(_isLoading.notifier).update((_) => false);

    if (errorMessage != null) {
      scaffoldMessengerKey.show(errorMessage,
          backgroundColor: Colors.redAccent);
    }

    if (successMessage != null) {
      scaffoldMessengerKey.show(successMessage, backgroundColor: Colors.blue);
    }
  };
});

Discussion

づだづだ

これするなら Navigator.pop() でええやんとなっております