You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Riverpod、GoRouter与Firebase认证时,登录查询Firestore用户文档触发组件状态错误的技术问询

错误原因分析与解决方案

错误原因

这个报错的核心问题是你在异步操作完成后,尝试使用一个已经被停用(deactivated)的Widget的BuildContext:

  • 当你调用登录方法时,登录成功后可能会立即触发导航,导致原SignIn Widget被移出Widget树(进入停用/销毁状态);但Firestore的get()是异步操作,当它执行完成时,你传入的context已经不再属于任何活跃的Widget,此时用这个context做导航或其他依赖Widget树的操作,就会触发Looking up a deactivated widget's ancestor is unsafe错误。
  • 另外你的signinProvider是autoDispose类型,当SignIn Widget销毁时,Provider会自动释放,如果异步操作此时还在进行,后续的状态更新也可能引发额外问题。

可行解决方案

我们通过分离业务逻辑与UI上下文、利用Riverpod状态监听处理导航的思路来解决问题:

1. 重构SigninController,移除对BuildContext的依赖

让控制器只负责登录逻辑和状态更新,不直接处理导航。先更新SigninState,加入登录后的状态标记:

enum SigninStatus { initial, loading, success, error }

class SigninState {
  // 保留你原有email、password等字段
  final SigninStatus status;
  final String? errorMessage;
  // 新增:标记是否需要完善资料
  final bool needsProfileSetup;

  SigninState({
    required this.status,
    this.errorMessage,
    this.needsProfileSetup = false,
    // 你的其他参数
  });

  // 实现copyWith方法(根据你的字段调整)
  SigninState copyWith({
    SigninStatus? status,
    String? errorMessage,
    bool? needsProfileSetup,
    // 你的其他字段
  }) {
    return SigninState(
      status: status ?? this.status,
      errorMessage: errorMessage ?? this.errorMessage,
      needsProfileSetup: needsProfileSetup ?? this.needsProfileSetup,
      // 你的其他字段拷贝
    );
  }
}

然后修改控制器的登录方法,移除context参数,改为通过状态传递结果:

class SigninController extends StateNotifier<SigninState> {
  final AuthenticationRepository _authRepo;
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;

  SigninController(this._authRepo) : super(SigninState(status: SigninStatus.initial));

  Future<void> signinWithEmailAndPassword() async {
    state = state.copyWith(status: SigninStatus.loading);
    try {
      await _authRepo.signInWithEmailAndPassword(
        state.email.value,
        state.password.value,
      );
      final currentUser = FirebaseAuth.instance.currentUser;
      if (currentUser != null) {
        final docSnapshot = await _firestore.collection('users').doc(currentUser.uid).get();
        state = state.copyWith(
          status: SigninStatus.success,
          needsProfileSetup: !docSnapshot.exists,
        );
      }
    } catch (e) {
      state = state.copyWith(
        status: SigninStatus.error,
        errorMessage: e.toString(),
      );
    }
  }

  // 同理重构signinGoogle方法
  Future<void> signinGoogle() async {
    state = state.copyWith(status: SigninStatus.loading);
    try {
      await _authRepo.signInWithGoogle();
      final currentUser = FirebaseAuth.instance.currentUser;
      if (currentUser != null) {
        final docSnapshot = await _firestore.collection('users').doc(currentUser.uid).get();
        state = state.copyWith(
          status: SigninStatus.success,
          needsProfileSetup: !docSnapshot.exists,
        );
      }
    } catch (e) {
      state = state.copyWith(
        status: SigninStatus.error,
        errorMessage: e.toString(),
      );
    }
  }

  // 你的onEmailChange、onPasswordChange等方法保留
}

2. 在UI层监听状态变化,处理导航

回到SignIn Widget,用ref.listen监听状态变化,在UI层处理导航,同时检查Widget是否活跃:

class SignIn extends ConsumerWidget {
  const SignIn({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef widgetRef) {
    // 监听登录状态,处理导航和错误提示
    widgetRef.listen<SigninState>(signinProvider, (previous, next) {
      if (next.status == SigninStatus.success) {
        // 确保Widget仍活跃再操作context
        if (mounted) {
          if (next.needsProfileSetup) {
            Navigator.pushNamed(context, FinishUpProfile.routeName);
            // 如果你用GoRouter,可以替换为:
            // widgetRef.read(goRouterProvider).push(FinishUpProfile.routeName);
          } else {
            Navigator.pushNamed(context, HomePage.routeName);
            // widgetRef.read(goRouterProvider).push(HomePage.routeName);
          }
        }
      } else if (next.status == SigninStatus.error && mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(next.errorMessage ?? '登录失败')),
        );
      }
    });

    // 你的原有UI代码保留
    final signinState = widgetRef.watch(signinProvider);
    final showEmailError = signinState.email.invalid ? Email.showEmailErrorMessege(signinState.email.error) : null;
    final showPasswordError = signinState.password.invalid ? Password.showPasswordErrorMessage(signinState.password.error) : null;
    final signinController = widgetRef.read(signinProvider.notifier);
    final bool _isValidated = signinState.status.isValidated;
    final screenSize = MediaQuery.of(context).size;

    return Column(
      children: [
        // 你的原有输入框、按钮等UI
        FFButtonWidget(
          onPressed: () async {
            if (!_isValidated) return;
            // 调用方法不再传context
            await signinController.signinWithEmailAndPassword();
          },
          // 其他按钮配置
        ),
        FFButtonWidget(
          onPressed: () async {
            final controller = widgetRef.read(signinProvider.notifier);
            controller.signinGoogle();
          },
          // 其他按钮配置
        ),
      ],
    );
  }
}

3. 额外优化:用Riverpod管理用户Firestore文档流

既然需要实时获取用户的Firestore文档,可以创建专门的StreamProvider来统一管理:

// 监听当前认证用户的流
final currentAuthUserProvider = StreamProvider<User?>((ref) {
  return FirebaseAuth.instance.authStateChanges();
});

// 监听对应用户的Firestore文档流
final currentFirestoreUserProvider = StreamProvider<FirestoreUser?>((ref) {
  final authUser = ref.watch(currentAuthUserProvider).value;
  if (authUser == null) return Stream.value(null);
  return FirebaseFirestore.instance
      .collection('users')
      .doc(authUser.uid)
      .snapshots()
      .map((snapshot) => snapshot.exists ? FirestoreUser.fromSnapshot(snapshot) : null);
});

之后在任何需要用户资料的地方,只需通过ref.watch(currentFirestoreUserProvider)就能获取实时更新的文档数据。


方案优势

  • 分离了业务逻辑与UI操作,符合单一职责原则,避免了控制器持有context带来的生命周期问题;
  • 通过ref.listen监听状态,结合mounted检查,确保只在Widget活跃时操作context,彻底解决了报错;
  • 用StreamProvider统一管理用户文档流,更贴合Riverpod的状态管理思路,也能实时响应文档变化。

内容的提问来源于stack exchange,提问作者Tekletsadik A. Tesfa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 16:52:42