Flutter Bloc+onGenerateRoute页面重复注入与导航栈重复推送问题
Flutter Bloc导航重复触发问题解决方案
问题核心原因
- 状态字段未重置:AuthBloc中
isOtpCodeValid字段在OTP验证成功后未重置为false,导致后续状态更新(比如重复点击按钮触发loading状态后回到success状态)时,BlocListener会重复检测到isOtpCodeValid为true,进而重复触发导航。 - listenWhen条件不严谨:当前仅判断字段是否变化,未限定
isOtpCodeValid的变化方向,只要前后状态该字段值不同就会触发导航(比如从true变回false再变回true)。
解决方案1:优化AuthBloc状态设计(推荐)
采用一次性状态类型替代bool字段,更符合Bloc状态驱动的设计原则,从根源避免状态残留问题。
修改AuthState结构
abstract class AuthState {} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} // 单独定义OTP验证成功的状态 class AuthOtpVerified extends AuthState { final bool isUserProfileDataFilled; AuthOtpVerified({required this.isUserProfileDataFilled}); } class AuthError extends AuthState { final String message; AuthError({required this.message}); }
调整BlocListener逻辑
BlocListener<AuthBloc, AuthState>( listener: (context, state) { if (state is AuthOtpVerified) { // 调用登录成功逻辑 _loginUser(); // 根据用户资料状态导航 if (!state.isUserProfileDataFilled) { Navigator.pushReplacementNamed(context, '/userDataInputScreen'); } else { Navigator.pushReplacementNamed(context, '/mainWrapper'); } } else if (state is AuthError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.message)), ); } }, child: BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 45.0), child: PrimaryButton( size: ButtonSize.small, label: 'VERIFY NOW', onTap: () { String otpCode = otpControllers.map((controller) => controller.text).join(); if(otpCode.length == 4){ context.read<AuthBloc>().add(AuthOtpSendCode(otpCode: otpCode)); } }, isLoading: state is AuthLoading, ), ); }, ), )
AuthBloc事件处理示例
on<AuthOtpSendCode>((event, emit) async { emit(AuthLoading()); try { // 执行OTP验证逻辑 final isProfileFilled = await checkUserProfileCompletion(); // 发出一次性验证成功状态 emit(AuthOtpVerified(isUserProfileDataFilled: isProfileFilled)); } catch (e) { emit(AuthError(message: e.toString())); } });
解决方案2:优化listenWhen条件并重置状态字段
如果不想修改状态结构,可通过以下方式修复:
调整listenWhen条件
仅在isOtpCodeValid从false变为true时触发导航:
listenWhen: (previous, current) { // 仅当OTP验证从无效变为有效时触发导航 final otpValidTrigger = !previous.isOtpCodeValid && current.isOtpCodeValid; // 错误信息变化时触发提示 final errorTrigger = previous.errorMessage != current.errorMessage; return otpValidTrigger || errorTrigger; },
在AuthBloc中重置状态字段
每次处理OTP事件时,先重置isOtpCodeValid为false,验证成功后短暂设置为true再重置:
on<AuthOtpSendCode>((event, emit) async { emit(state.copyWith( isLoading: true, isOtpCodeValid: false, errorMessage: null )); try { final isProfileFilled = await checkUserProfile(); // 发出验证成功状态 emit(state.copyWith( isLoading: false, isOtpCodeValid: true, isUserProfileDataFilled: isProfileFilled, )); // 延迟重置,避免后续状态更新触发重复导航 Future.delayed(Duration.zero, () { emit(state.copyWith(isOtpCodeValid: false)); }); } catch (e) { emit(state.copyWith( isLoading: false, errorMessage: e.toString(), )); } });
额外修复点
- 调用登录成功逻辑:在验证成功后必须调用
_loginUser(),否则SharedPreferences不会更新,下次启动App无法正确跳转:
if (state.isOtpCodeValid && !state.isUserProfileDataFilled) { _loginUser(); Navigator.pushReplacementNamed(context, '/userDataInputScreen'); } else if(state.isOtpCodeValid){ _loginUser(); Navigator.pushReplacementNamed(context, '/mainWrapper'); }
- 清理AppRouter冗余代码:
getInitialRoute方法中有多余的return语句,需删除:
Future<String> getInitialRoute() async { SharedPreferences prefs = await SharedPreferences.getInstance(); bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false; bool isFirstTime = prefs.getBool('isFirstTime') ?? true; if (isLoggedIn) { return mainWrapper; } else if (isFirstTime) { return onboarding1; } else { return signInScreen; } }
- 控制器内存管理:将OtpScreen改为StatefulWidget,在
dispose中释放TextEditingController,避免内存泄漏:
class OtpScreen extends StatefulWidget { const OtpScreen({super.key}); @override State<OtpScreen> createState() => _OtpScreenState(); } class _OtpScreenState extends State<OtpScreen> { late final List<TextEditingController> otpControllers; @override void initState() { super.initState(); otpControllers = List.generate(4, (index) => TextEditingController()); } @override void dispose() { for (var controller in otpControllers) { controller.dispose(); } super.dispose(); } // 剩余build逻辑不变 }
内容的提问来源于stack exchange,提问作者sunny 4k4
相关产品推荐
相关产品推荐

