如何在Flutter中用BLOC实现单屏认证的组件显隐控制
实现基于BLoC的单屏登录/注册流程(动态组件切换)
核心思路是通过统一的Auth状态管理,用状态枚举标识当前流程阶段,UI完全根据状态动态渲染对应组件,实现无跳转的单屏认证流程。
一、重构状态管理:创建统一的AuthBloc和AuthState
将原有的LoginBloc、SignUpBloc合并为AuthBloc,用枚举定义流程状态,集中管理所有认证相关字段和流程逻辑。
1. 定义AuthState和流程枚举
// 标识当前认证流程阶段 enum AuthFlow { initial, // 初始状态:仅显示邮箱输入框 loginRequired, // 邮箱已存在:显示登录字段 signupRequired // 邮箱不存在:显示注册字段 } class AuthState { final AuthFlow flow; final String email; // 登录专用字段 final String loginPassword; // 注册专用字段 final String firstName; final String middleName; final String signupPassword; AuthState({ this.flow = AuthFlow.initial, this.email = '', this.loginPassword = '', this.firstName = '', this.middleName = '', this.signupPassword = '', }); AuthState copyWith({ AuthFlow? flow, String? email, String? loginPassword, String? firstName, String? middleName, String? signupPassword, }) { return AuthState( flow: flow ?? this.flow, email: email ?? this.email, loginPassword: loginPassword ?? this.loginPassword, firstName: firstName ?? this.firstName, middleName: middleName ?? this.middleName, signupPassword: signupPassword ?? this.signupPassword, ); } }
2. 实现AuthBloc和事件处理
将原AuthController的业务逻辑迁移到Bloc中,通过事件驱动状态更新:
abstract class AuthEvent {} // 输入邮箱事件 class AuthEmailChangedEvent extends AuthEvent { final String email; AuthEmailChangedEvent(this.email); } // 验证邮箱是否存在事件 class AuthCheckEmailEvent extends AuthEvent {} // 登录密码输入事件 class AuthLoginPasswordChangedEvent extends AuthEvent { final String password; AuthLoginPasswordChangedEvent(this.password); } // 注册字段输入事件 class AuthSignupFirstNameChangedEvent extends AuthEvent { final String firstName; AuthSignupFirstNameChangedEvent(this.firstName); } class AuthSignupMiddleNameChangedEvent extends AuthEvent { final String middleName; AuthSignupMiddleNameChangedEvent(this.middleName); } class AuthSignupPasswordChangedEvent extends AuthEvent { final String password; AuthSignupPasswordChangedEvent(this.password); } // 登录提交事件 class AuthLoginSubmittedEvent extends AuthEvent {} // 注册提交事件 class AuthSignupSubmittedEvent extends AuthEvent {} class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc() : super(AuthState()) { on<AuthEmailChangedEvent>(_onEmailChanged); on<AuthCheckEmailEvent>(_onCheckEmail); on<AuthLoginPasswordChangedEvent>(_onLoginPasswordChanged); on<AuthSignupFirstNameChangedEvent>(_onSignupFirstNameChanged); on<AuthSignupMiddleNameChangedEvent>(_onSignupMiddleNameChanged); on<AuthSignupPasswordChangedEvent>(_onSignupPasswordChanged); on<AuthLoginSubmittedEvent>(_onLoginSubmitted); on<AuthSignupSubmittedEvent>(_onSignupSubmitted); } void _onEmailChanged(AuthEmailChangedEvent event, Emitter<AuthState> emit) { emit(state.copyWith(email: event.email)); } Future<void> _onCheckEmail(AuthCheckEmailEvent event, Emitter<AuthState> emit) async { if (state.email.isEmpty) { toastInfo(msg: "请输入邮箱地址"); return; } try { AuthModel authModel = AuthModel()..email = state.email; final response = await AuthRepository.preview(authModel); if (response.response!.code == 200) { // 邮箱存在,切换到登录流程 emit(state.copyWith(flow: AuthFlow.loginRequired)); } else { // 邮箱不存在,切换到注册流程 emit(state.copyWith(flow: AuthFlow.signupRequired)); } } catch (e) { toastInfo(msg: e.toString()); } } void _onLoginPasswordChanged(AuthLoginPasswordChangedEvent event, Emitter<AuthState> emit) { emit(state.copyWith(loginPassword: event.password)); } void _onSignupFirstNameChanged(AuthSignupFirstNameChangedEvent event, Emitter<AuthState> emit) { emit(state.copyWith(firstName: event.firstName)); } void _onSignupMiddleNameChanged(AuthSignupMiddleNameChangedEvent event, Emitter<AuthState> emit) { emit(state.copyWith(middleName: event.middleName)); } void _onSignupPasswordChanged(AuthSignupPasswordChangedEvent event, Emitter<AuthState> emit) { emit(state.copyWith(signupPassword: event.password)); } Future<void> _onLoginSubmitted(AuthLoginSubmittedEvent event, Emitter<AuthState> emit) async { if (state.email.isEmpty) { toastInfo(msg: "邮箱地址不能为空"); return; } if (state.loginPassword.isEmpty) { toastInfo(msg: "密码不能为空"); return; } try { LoginModel loginModel = LoginModel() ..email = state.email ..password = state.loginPassword; final response = await AuthRepository.login(loginModel); if (response.response!.code == 200) { toastInfo(msg: response.response!.message!); if (context.mounted) { Navigator.of(context).pushNamedAndRemoveUntil(AppRoutes.HOME, (route) => false); } } else { toastInfo(msg: response.response!.message!); } } catch (e) { toastInfo(msg: e.toString()); } } Future<void> _onSignupSubmitted(AuthSignupSubmittedEvent event, Emitter<AuthState> emit) async { if (state.firstName.isEmpty) { toastInfo(msg: "请输入名字"); return; } if (state.email.isEmpty) { toastInfo(msg: "请输入邮箱地址"); return; } if (state.signupPassword.isEmpty) { toastInfo(msg: "请输入密码"); return; } try { showLoaderDialog(context); SignUpModel signUpModel = SignUpModel() ..firstName = state.firstName ..middleName = state.middleName ..email = state.email ..password = state.signupPassword; final response = await AuthRepository.signUp(signUpModel); if (response.response!.code == 200) { toastInfo(msg: response.response!.message!); if (context.mounted) { Navigator.of(context).pushNamedAndRemoveUntil(AppRoutes.HOME, (route) => false); } } } catch (e) { toastInfo(msg: e.toString()); } finally { if (context.mounted) Navigator.pop(context); } } }
二、UI层动态渲染组件
修改LoginScreen,通过BlocBuilder监听AuthBloc状态,根据AuthFlow动态显示对应字段和按钮:
class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { @override Widget build(BuildContext context) { return BlocBuilder<AuthBloc, AuthState>(builder: (context, state) { return Scaffold( backgroundColor: const Color(0xFFF1F3F6), body: SafeArea( child: Container( margin: const EdgeInsets.symmetric(horizontal: 100), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const SizedBox(height: 50), Center( child: Text( state.flow == AuthFlow.signupRequired ? "注册" : "登录", style: const TextStyle( color: Color(0xFF4D70A6), fontSize: 26, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 40), // 始终显示邮箱输入框 buildTextField("邮箱", "", (value) { context.read<AuthBloc>().add(AuthEmailChangedEvent(value)); }), const SizedBox(height: 40), // 根据流程状态显示对应字段 if (state.flow == AuthFlow.loginRequired) buildTextField("密码", "password", (value) { context.read<AuthBloc>().add(AuthLoginPasswordChangedEvent(value)); }), if (state.flow == AuthFlow.signupRequired) ...[ buildTextField("名字", "", (value) { context.read<AuthBloc>().add(AuthSignupFirstNameChangedEvent(value)); }), const SizedBox(height: 40), buildTextField("中间名", "", (value) { context.read<AuthBloc>().add(AuthSignupMiddleNameChangedEvent(value)); }), const SizedBox(height: 40), buildTextField("密码", "password", (value) { context.read<AuthBloc>().add(AuthSignupPasswordChangedEvent(value)); }), ], const SizedBox(height: 50), // 动态切换按钮文本和事件 GestureDetector( onTap: () { if (state.flow == AuthFlow.initial) { context.read<AuthBloc>().add(AuthCheckEmailEvent()); } else if (state.flow == AuthFlow.loginRequired) { context.read<AuthBloc>().add(AuthLoginSubmittedEvent()); } else if (state.flow == AuthFlow.signupRequired) { context.read<AuthBloc>().add(AuthSignupSubmittedEvent()); } }, child: Container( width: double.infinity, alignment: Alignment.center, height: 50, margin: const EdgeInsets.symmetric(vertical: 15), decoration: BoxDecoration( color: const Color(0xFFF1F3F6), borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( offset: const Offset(10, 10), color: const Color(0xFF4D70A6).withOpacity(0.2), blurRadius: 16, ), const BoxShadow( offset: Offset(-10, -10), color: Color.fromARGB(170, 255, 255, 255), blurRadius: 10, ), ], ), child: Text( state.flow == AuthFlow.initial ? "验证邮箱" : state.flow == AuthFlow.loginRequired ? "登录" : "注册", style: const TextStyle(color: Color(0xFF4D70A6), fontSize: 16), ), ), ), ], ), ), ), ); }); } // 复用的TextField组件 Widget buildTextField(String label, String obscureTextType, Function(String) onChanged) { return TextField( decoration: InputDecoration( labelText: label, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), ), obscureText: obscureTextType == "password", onChanged: onChanged, ); } }
三、关键优化点
- 状态驱动UI:UI完全由
AuthState.flow决定,避免手动控制组件显隐的混乱逻辑 - 统一状态管理:所有认证字段和流程状态集中在AuthState,消除多Bloc状态同步问题
- 事件驱动逻辑:用户操作转化为Bloc事件,业务逻辑与UI完全解耦
四、清理冗余代码
删除原有的LoginBloc、SignUpBloc和AuthController,所有逻辑已整合到AuthBloc中。
内容的提问来源于stack exchange,提问作者Ukeme Elijah
相关产品推荐
相关产品推荐

