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

Flutter Bloc/Cubit集成Firebase注册时遇“查找已销毁Widget祖先不安全”错误

Flutter集成Bloc/Cubit与Firebase注册时的"已销毁组件祖先查找不安全"问题

错误信息

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:

  1. 在注册页面的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),
)
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 05:54:09