Flutter Bloc/Cubit集成Firebase注册时遇“查找已销毁Widget祖先不安全”错误
错误信息
Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
问题描述
错误指向CustomButton组件中的这段代码:
context.read<SignupCubit>().signupWithCredentials().whenComplete(() { User user = User( id: context.read<SignupCubit>().state.user!.uid, name: '', age: 0, );
调试模式下无异常,逐行运行流程正常,但非调试模式中出现该错误,求助解决。
相关代码
CustomButton组件
注册页面按钮,点击后创建Firebase用户并生成用户文档:
class CustomButton extends StatelessWidget { final TabController? tabController; final String text; CustomButton({ Key? key, this.tabController, this.text = 'START', }) : super(key: key); @override Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), gradient: LinearGradient( colors: [ Theme.of(context).primaryColor, Theme.of(context).primaryColorLight ], ), ), child: ElevatedButton( style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: Colors.transparent), onPressed: () { if (tabController!.index <= 4) { tabController!.animateTo(tabController!.index + 1); } else { Navigator.pushNamed(context, '/'); } if (tabController!.index == 2) { context .read<SignupCubit>() .signupWithCredentials() .whenComplete(() { User user = User( id: context.read<SignupCubit>().state.user!.uid, name: '', age: 0, ); context .read<OnboardingBloc>() .add(StartOnboarding(user: user)); }); } }, child: SizedBox( child: Center( child: Text( text, style: Theme.of(context) .textTheme .headline4! .copyWith(color: Colors.white), ), ), width: double.infinity, ), )); } }
SignupCubit
class SignupCubit extends Cubit<SignupState> { final AuthRepository? _authRepository; SignupCubit({ AuthRepository? authRepository}) : _authRepository = authRepository, super(SignupState.initial()); void emailChanged(String value) { emit(state.copywith(email: value, status: SignupStatus.initial)); } void passwordChanged(String value) { emit(state.copywith(password: value, status: SignupStatus.initial)); } Future<void> signupWithCredentials() async { if (!state.isValid || state.status == SignupStatus.submitting) return; emit(state.copywith(status: SignupStatus.submitting)); try { var user = await _authRepository!.signUp( email: state.email, password: state.password, ); emit(state.copywith( status: SignupStatus.success, user: user, )); } catch (e) { debugPrint('$e'); } } }
SignupState
enum SignupStatus { initial, submitting, success, error } class SignupState extends Equatable { final String email; final String password; final SignupStatus status; final auth.User? user; const SignupState( {required this.email, required this.password, required this.status, this.user, }); factory SignupState.initial() { return const SignupState( email: '', password: '', status: SignupStatus.initial, user: null,); } SignupState copywith({ String? email, String? password, SignupStatus? status, auth.User? user, String? uid, }) { return SignupState( email: email ?? this.email, password: password ?? this.password, status: status ?? this.status, user: user ?? this.user, ); } bool get isValid => email.isNotEmpty && password.isNotEmpty; @override bool get stringify => true; @override List<Object?> get props => [email, password, status, user]; }
AuthRepository
class AuthRepository extends BaseAuthRepository { final auth.FirebaseAuth _firebaseAuth; AuthRepository({auth.FirebaseAuth? firebaseAuth}) : _firebaseAuth = firebaseAuth ?? auth.FirebaseAuth.instance; @override Future<auth.User?> signUp( {required String email, required String password}) async { final credential = await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password); final user = credential.user; return user; } }
问题原因与解决方案
原因
whenComplete回调执行时,CustomButton所在的Widget可能已经被销毁(比如Tab切换或页面导航导致组件移除),此时调用context.read()会尝试访问已销毁组件的祖先,触发不安全访问警告。
解决方法
方法1:提前获取Cubit实例,避免回调中使用context
在触发异步操作前,先缓存Cubit和Bloc实例,回调中直接使用缓存的实例:
if (tabController!.index == 2) { // 提前获取实例,避免回调中通过context查找 final signupCubit = context.read<SignupCubit>(); final onboardingBloc = context.read<OnboardingBloc>(); signupCubit.signupWithCredentials().whenComplete(() { User user = User( id: signupCubit.state.user!.uid, name: '', age: 0, ); onboardingBloc.add(StartOnboarding(user: user)); }); }
方法2:用BlocListener监听状态变化(推荐)
符合Bloc/Cubit最佳实践,通过状态监听处理后续逻辑,彻底避免异步回调中使用context:
- 在注册页面的Widget树中添加
BlocListener:
BlocListener<SignupCubit, SignupState>( listener: (context, state) { // 监听注册成功状态,执行后续操作 if (state.status == SignupStatus.success && state.user != null) { final user = User( id: state.user!.uid, name: '', age: 0, ); context.read<OnboardingBloc>().add(StartOnboarding(user: user)); } }, child: CustomButton(tabController: tabController), )
- 修改
CustomButton的onPressed逻辑,仅触发注册操作:
if (tabController!.index == 2) { context.read<SignupCubit>().signupWithCredentials(); }
方法3:判断Widget是否挂载(应急方案)
如果必须在回调中使用context,可通过mounted属性判断当前组件是否还在树中(仅适用于StatefulWidget,需将CustomButton改为Stateful):
class CustomButton extends StatefulWidget { // ... 保留原有参数 @override State<CustomButton> createState() => _CustomButtonState(); } class _CustomButtonState extends State<CustomButton> { @override Widget build(BuildContext context) { return DecoratedBox( // ... 原有UI代码 child: ElevatedButton( onPressed: () { // ... 原有Tab切换逻辑 if (widget.tabController!.index == 2) { context.read<SignupCubit>().signupWithCredentials().whenComplete(() { // 先判断组件是否还挂载 if (!mounted) return; User user = User( id: context.read<SignupCubit>().state.user!.uid, name: '', age: 0, ); context.read<OnboardingBloc>().add(StartOnboarding(user: user)); }); } }, // ... 原有子组件代码 ), ); } }
内容的提问来源于stack exchange,提问作者yuyu

