【Flutter】Riverpodを使ってダイアログを表示するときの注意点

# はじめに
小ネタ的な記事となりますが、Riverpodでwatchを使ってダイアログを表示するとエラーになったので回避策について書こうと思います。


# エラーになったコード
```dart
// お知らせデータ一覧を取得するFutureProvider
final notificationsProvider = FutureProvider<List<String>>(
  (ref) async {
    await Future.delayed(
      const Duration(
        seconds: 1,
      ),
    );

    // エラーになったとする
    if (true) {
      throw Exception("notifications error");
    }

    return [
      "notification1",
      "notification2",
    ];
  },
);

class ErrorHandlingPage extends ConsumerWidget {
  const ErrorHandlingPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // FutureProviderをwatch
    final notifications = ref.watch(notificationsProvider);

    // エラーがあったら、ダイアログを表示
    if (notifications.hasError) {
      showAdaptiveDialog(
        context: context,
        builder: (dialogContext) {
          return AlertDialog.adaptive(
            title: const Text("Error"),
            content: Text(notifications.error.toString()),
            actions: [
              TextButton(
                child: const Text("OK"),
                onPressed: () {
                  Navigator.of(dialogContext).pop();
                },
              ),
            ],
          );
        },
      );
    }

    // 以下でnotificationsを参照して、画面作成（省略）
    return Scaffold(）;
.......続く
```

上記のように、watchした値でエラー判定してダイアログを出すと、`setState() or markNeedsBuild() called during build.`というエラーになります。

# 回避策1
上記のエラーになった原因ですが、ウィジェットのビルドフェーズ中に新しいダイアログを表示しようとしていることが原因です。
回避するには以下のように`WidgetsBinding.instance.addPostFrameCallback`で囲むといいです。
```dart
  // ダイアログ表示部分を囲むと、build後に実行される 
  WidgetsBinding.instance.addPostFrameCallback((_) {
      showAdaptiveDialog(
        context: context,
        builder: (dialogContext) {
          return AlertDialog.adaptive(
            title: const Text("Error"),
            content: Text(notifications.error.toString()),
            actions: [
              TextButton(
                child: const Text("OK"),
                onPressed: () {
                  Navigator.of(dialogContext).pop();
                },
              ),
            ],
          );
        },
      );
    });
```

# 回避策2
もう一つの解決方法としてlistenを使う手もあります。

```diff_dart
  @override
  Widget build(BuildContext context, WidgetRef ref) {
-    final notifications = ref.watch(notificationsProvider);
-    if (notifications.hasError) {
-      showAdaptiveDialog(
-        context: context,
-        builder: (dialogContext) {
-          return AlertDialog.adaptive(
-            title: const Text("Error"),
-            content: Text(notifications.error.toString()),
-            actions: [
-              TextButton(
-                child: const Text("OK"),
-                onPressed: () {
-                  Navigator.of(dialogContext).pop();
-                },
-              ),
-            ],
-          );
-        },
-      );
-    }
+    ref.listen(notificationsProvider, (previous, next) {
+      if (previous == next) {
+        return;
+      }
+
+      if (next.hasError) {
+        showAdaptiveDialog(
+          context: context,
+          builder: (dialogContext) {
+            return AlertDialog.adaptive(
+              title: const Text("Error"),
+              content: Text(next.error.toString()),
+              actions: [
+                TextButton(
+                  child: const Text("OK"),
+                  onPressed: () {
+                    Navigator.of(dialogContext).pop();
+                  },
+                ),
+              ],
+            );
+          },
+        );
+      }
+    });    

    return Scaffold();
  }
```

`ref.listen`を使用すると、指定したプロバイダの状態が変更されたときにコールバックが呼び出されます。このコールバックは、ウィジェットのbuildメソッドの外で実行されるため、buildメソッドの実行中にウィジェットの状態を変更することはありません。