🙆

buildメソッド内で副作用を起こすべきではない?

2024/02/29に公開

こんにちは、タオルです。

備忘録兼、メモとしてこちら記入しているため、不確かな情報です。

Riverpodで、Firebase auth stateをstreamでキャッチして、ログイン情報があればHomePageに遷移。ログイン情報がなければDashboardPageに遷移するというコードを書いたつもりです。

生成AIに、buildの中で、ナビゲーションするなと怒られました。
ナビゲーションは副作用に該当しており、描画中に副作用を起こすべきではないということ。

副作用を避けるには、ref.listenを使って、ConsumerStatefullWidgetを使うか、

ref.watchのクラスをWidgetsBinding.instance.addPostFrameCallback((_) {}で囲って、buildが走った後に、ナビゲーションが起こるようにするらしい。

だけど、そういったことが公式ドキュメントのどこに書いてあるかがよくわからん。

自分のしたい実装が特殊ケースなのだろうか・・・。

first_page
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_question_answer/router/auth_navigator.dart';

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

  
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends ConsumerState<FirstPage> {
  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final authNavigator = AuthNavigator(ref);
      authNavigator.navigateBasedOnAuthState(context);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Stack(
          children: <Widget>[
            Container(
              decoration: const BoxDecoration(
                image: DecorationImage(
                  image: AssetImage('assets/test12.jpg'),
                  fit: BoxFit.cover,
                ),
              ),
            ),
            // 他のウィジェット...
          ],
        ),
      ),
    );
  }
}
auth_navigator
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_question_answer/auth_state_notifier.dart';
import 'package:flutter_question_answer/router/app_router.dart';

class AuthNavigationController {
  static void navigateBasedOnAuthState(BuildContext context, WidgetRef ref) {
    ref.listen<AsyncValue<dynamic>>(authStateNotifierProvider, (_, state) {
      state.when(
        data: (user) {
          if (user != null) {
            context.router.replace(HomeRoute());
          } else {
            context.router.replace(const DashboardRoute());
          }
        },
        loading: () => {},
        error: (_, __) => {},
      );
    });
  }
}
auth_state_notifier
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'auth_state_notifier.g.dart';


class AuthStateNotifier extends _$AuthStateNotifier {
  
  Stream<User?> build() {
    // FirebaseAuthのauthStateChangesをリッスンし、ユーザーの状態変更を監視
    return FirebaseAuth.instance.authStateChanges();
  }
}

以下 AIによる説法。

ConsumerWidgetのbuildメソッド内でwidget.watchを使用することは、プロバイダーの値を監視するための正しい方法です。しかし、buildメソッド内で直接ナビゲーションをトリガーすることは推奨されません。buildメソッドはウィジェットのビルドプロセス中に何度も呼び出される可能性があり、その結果、ナビゲーションが意図しないタイミングで複数回トリガーされる可能性があります。
ナビゲーションのような副作用を引き起こす操作は、プロバイダーの値の変更を監視するためにref.watchを使用する場合、以下のような方法で安全に行うべきです:

  1. ConsumerStatefulWidgetとConsumerStateを使用する: ConsumerStatefulWidgetを使用し、その状態管理クラス(ConsumerState)内でref.listenを使ってプロバイダーの値の変更を監視し、その変更に応じてナビゲーションを行います。この方法では、WidgetsBinding.instance.addPostFrameCallbackを使用して、ビルドプロセスが完了した後にナビゲーションを実行することができます。
  2. ref.listenを適切に使用する: ConsumerWidgetのbuildメソッド内ではなく、アプリケーションの他の部分でref.listenを使用してプロバイダーの値の変更を監視し、ナビゲーションをトリガーします。例えば、アプリケーションのルートレベルでref.listenを使用することができます。
    ナビゲーションを含む副作用を安全に扱うための一般的なガイドラインは、それらを直接buildメソッド内で実行するのではなく、ウィジェットのライフサイクルイベントやref.listenのコールバック内で実行することです。これにより、ウィジェットのビルドプロセスと副作用の実行を適切に分離し、予期しない挙動を避けることができます。

Discussion