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

如何在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,
    );
  }
}

三、关键优化点

  1. 状态驱动UI:UI完全由AuthState.flow决定,避免手动控制组件显隐的混乱逻辑
  2. 统一状态管理:所有认证字段和流程状态集中在AuthState,消除多Bloc状态同步问题
  3. 事件驱动逻辑:用户操作转化为Bloc事件,业务逻辑与UI完全解耦

四、清理冗余代码

删除原有的LoginBloc、SignUpBloc和AuthController,所有逻辑已整合到AuthBloc中。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 05:44:51