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

Bloc重复触发同一状态的原因及代码修复方案

问题

我是Bloc新手,发现单次状态变更会触发多次状态变化,比如执行登录或登出操作时会弹出多个提示Snackbar。操作功能正常,但登出时会连续弹出5个“Logged out successfully”提示。以下是我的代码实现:

login_page.dart中的BlocListener

BlocListener<AuthBloc, AuthState>(
  listenWhen: (previous, current) => previous != current,
  listener: (context, state) {
    dev.log('listener login page: $state');

    if (state is AuthLoadingState) {
      loadingDialog(context);
    } else if (state is AuthLoadedState) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(
          builder: (context) => const HomePage(),
        ),
      );
      showSnackBar(
          message: 'Logged in successfully', isError: false);
    } else if (state is AuthErrorState) {
      Navigator.of(context).pop();
      showSnackBar(message: state.message, isError: true);
    }
  },
  child: AuthButtonWidget(
    formKey: _formKey,
    onPressed: () {
      if (_formKey.currentState!.validate()) {
        BlocProvider.of<AuthBloc>(context).add(
          LogInWithEmailAndPasswordEvent(
            email: _emailController.text,
            password: _passwordController.text,
          ),
        );
      }
    },
    text: 'Log In',
  ),
),

home_page.dart中的BlocConsumer

BlocConsumer<AuthBloc, AuthState>(
  listenWhen: (previous, current) => previous != current,
  listener: (context, state) {
    dev.log('listener home page: $state');
    if (state is AuthLoadingState) {
      loadingDialog(context);
    } else if (state is AuthLoggedOutState) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(
          builder: (context) => const LoginPage(),
        ),
      );
      showSnackBar(message: 'Logged out successfully', isError: false);
    } else if (state is AuthErrorState) {
      Navigator.of(context).pop();
      showSnackBar(message: state.message, isError: true);
    }
  },
  buildWhen: (previous, current) => previous != current,
  builder: (context, state) {
    dev.log('builder home page: $state');

    if (state is AuthLoadedState) {
      return const HomePageWidget();
    } else if (state is AuthErrorState) {
      return const Center(
        child: Text('AuthErrorState'),
      );
    } else {
      return const HomePageWidget();
    }
  },
);

AuthBloc实现

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final SignUpWithEmailAndPasswordUsecase signUpWithEmailAndPassword;
  final LogInWithEmailAndPasswordUsecase logInWithEmailAndPassword;
  final LogOutUsecase logOut;
  AuthBloc(
    this.signUpWithEmailAndPassword,
    this.logInWithEmailAndPassword,
    this.logOut,
  ) : super(AuthInitial()) {
    on<AuthEvent>((event, emit) async {
      if (event is SignUpWithEmailAndPasswordEvent) {
        emit(AuthLoadingState());

        final failureOrUnit = await signUpWithEmailAndPassword(
          email: event.email,
          password: event.password,
          name: event.name,
        );

        emit(_mapFailureOrPostsState(either: failureOrUnit));
      } else if (event is LogInWithEmailAndPasswordEvent) {
        emit(AuthLoadingState());

        final failureOrUnit = await logInWithEmailAndPassword(
          email: event.email,
          password: event.password,
        );

        emit(_mapFailureOrPostsState(either: failureOrUnit));
      } else if (event is LogOutEvent) {
        emit(AuthLoadingState());

        final failureOrUnit = await logOut();

        emit(_mapFailureOrPostsState(
          either: failureOrUnit,
          isLogOut: true,
        ));
      }
    });
  }
}

AuthState _mapFailureOrPostsState({
  required Either<Failure, Unit> either,
  bool isLogOut = false,
}) {
  return either.fold(
    (failure) => AuthErrorState(message: _mapFailureToMessage(failure)),
    (_) {
      if (isLogOut) {
        return AuthLoggedOutState();
      } else {
        return AuthLoadedState();
      }
    },
  );
}

String _mapFailureToMessage(Failure failure) {
  if (failure is SignUpWithEmailAndPasswordFailure) {
    switch (failure.code) {
      case 'email-already-in-use':
        return EMAIL_ALREADY_IN_USE_FAILURE_MESSAGE;
      case 'invalid-email':
        return INVALID_EMAIL_FAILURE_MESSAGE;
      case 'operation-not-allowed':
        return OPERATION_NOT_ALLOWED_FAILURE_MESSAGE;
      case 'weak-password':
        return WEAK_PASSWORD_FAILURE_MESSAGE;
      default:
        return SIGN_UP_WITH_EMAIL_AND_PASSWORD_FAILURE_MESSAGE;
    }
  } else if (failure is LogInWithEmailAndPasswordFailure) {
    switch (failure.code) {
      case 'invalid-email':
        return INVALID_EMAIL_FAILURE_MESSAGE;
      case 'user-disabled':
        return USER_IS_DISABLED_FAILURE_MESSAGE;
      case 'user-not-found':
        return USER_NOT_FOUND_FAILURE_MESSAGE;
      case 'wrong-password':
        return WRONG_PASSWORD_FAILURE_MESSAGE;
      default:
        return LOG_IN_WITH_EMAIL_AND_PASSWORD_FAILURE_MESSAGE;
    }
  } else if (failure is LogOutFailure) {
    return LOG_OUT_FAILURE_MESSAGE;
  } else if (failure is OfflineFailure) {
    return OFFLINE_FAILURE_MESSAGE;
  } else if (failure is EmptyCacheFailure) {
    return EMPTY_CACHE_FAILURE_MESSAGE;
  } else {
    return UNEXPECTED_FAILURE_MESSAGE;
  }
}

原因分析
  1. 多监听者重复响应:登录页的BlocListener和首页的BlocConsumer都监听同一个AuthBloc,页面跳转后旧页面的监听者未被正确销毁,仍会接收状态变更触发回调。
  2. listenWhen条件太宽松:previous != current会监听所有状态变化,包括Loading到最终状态的切换,页面跳转后旧监听者仍会响应后续状态,重复执行showSnackBar。
  3. Widget重建导致监听重复注册:如果BlocListener/BlocConsumer所在Widget频繁重建,会多次注册监听,每次重建都会触发状态回调。

修复方案

1. 精准控制listenWhen条件

只在目标状态首次触发时执行回调,避免不必要的响应:

登录页BlocListener修改

BlocListener<AuthBloc, AuthState>(
  listenWhen: (previous, current) {
    // 只监听Loading、Loaded、Error状态的首次切换
    return (current is AuthLoadingState && previous is! AuthLoadingState) ||
           (current is AuthLoadedState && previous is! AuthLoadedState) ||
           (current is AuthErrorState && previous is! AuthErrorState);
  },
  listener: (context, state) {
    dev.log('listener login page: $state');
    // 原有逻辑不变
  },
  // 其余代码不变
),

首页BlocConsumer修改

BlocConsumer<AuthBloc, AuthState>(
  listenWhen: (previous, current) {
    // 只监听Loading、LoggedOut、Error状态的首次切换
    return (current is AuthLoadingState && previous is! AuthLoadingState) ||
           (current is AuthLoggedOutState && previous is! AuthLoggedOutState) ||
           (current is AuthErrorState && previous is! AuthErrorState);
  },
  listener: (context, state) {
    dev.log('listener home page: $state');
    // 原有逻辑不变
  },
  // buildWhen保持或按需调整
  buildWhen: (previous, current) => previous != current,
  builder: (context, state) {
    // 原有逻辑不变
  },
);

2. 确保SnackBar上下文正确

使用当前页面的ScaffoldMessenger显示SnackBar,避免上下文错误导致重复弹出:

// 修改showSnackBar实现
void showSnackBar(BuildContext context, {required String message, required bool isError}) {
  final snackBar = SnackBar(
    content: Text(message),
    backgroundColor: isError ? Colors.red : Colors.green,
  );
  ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

调用时传入当前上下文:showSnackBar(context, message: 'Logged out successfully', isError: false);

3. 优化Bloc作用域(可选)

如果AuthBloc仅用于登录/首页流程,可在登录页父级提供Bloc,而非全局提供,页面跳转后Bloc会被销毁,避免旧监听者残留。


内容的提问来源于stack exchange,提问作者Mikelenjilo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 03:12:05