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

Flutter BLoC v6实现带验证登录表单及按钮激活问题求助

我完全懂你的困惑——自定义BLoC刚摸明白,转官方flutter_bloc库还赶上版本大更新,旧教程全失效,确实头大。我给你写一个**flutter_bloc v6+**的完整登录表单示例,正好解决你最关心的「只有两个字段都验证通过才激活提交按钮」的需求。

先理清核心逻辑

在flutter_bloc的范式里,我们需要:

  1. 事件(Event):描述用户的操作(输入邮箱、输入密码、点击登录)
  2. 状态(State):描述当前表单的状态(邮箱内容、密码内容、各自的错误、表单是否可提交)
  3. Bloc:接收事件,处理业务逻辑(验证规则),输出新状态

第一步:配置依赖

先在pubspec.yaml里添加最新的依赖(确保是v6+版本):

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^6.1.1 # 或更高稳定版
  equatable: ^2.0.3 # 用于简化状态/事件的相等比较

第二步:定义事件(LoginEvent)

我们需要三个事件:邮箱输入变化、密码输入变化、提交登录。用Equatable让事件可以被Bloc正确识别和比较:

import 'package:equatable/equatable.dart';

abstract class LoginEvent extends Equatable {
  const LoginEvent();

  @override
  List<Object> get props => [];
}

// 邮箱输入变化事件
class EmailChanged extends LoginEvent {
  final String email;

  const EmailChanged({required this.email});

  @override
  List<Object> get props => [email];
}

// 密码输入变化事件
class PasswordChanged extends LoginEvent {
  final String password;

  const PasswordChanged({required this.password});

  @override
  List<Object> get props => [password];
}

// 提交登录事件
class LoginSubmitted extends LoginEvent {}

第三步:定义状态(LoginState)

状态要包含表单的所有关键信息,这样UI能根据状态实时更新:

import 'package:equatable/equatable.dart';

abstract class LoginState extends Equatable {
  const LoginState();

  @override
  List<Object> get props => [];
}

// 初始状态
class LoginInitial extends LoginState {}

// 表单输入状态(核心状态)
class LoginInputState extends LoginState {
  final String email;
  final String password;
  final String? emailError; // 邮箱错误信息,null代表无错误
  final String? passwordError; // 密码错误信息,null代表无错误
  final bool isFormValid; // 表单是否可提交(两个字段都验证通过)

  const LoginInputState({
    required this.email,
    required this.password,
    this.emailError,
    this.passwordError,
    required this.isFormValid,
  });

  @override
  List<Object> get props => [email, password, isFormValid];

  // 方便复制状态,只修改需要更新的字段
  LoginInputState copyWith({
    String? email,
    String? password,
    String? emailError,
    String? passwordError,
    bool? isFormValid,
  }) {
    return LoginInputState(
      email: email ?? this.email,
      password: password ?? this.password,
      emailError: emailError ?? this.emailError,
      passwordError: passwordError ?? this.passwordError,
      isFormValid: isFormValid ?? this.isFormValid,
    );
  }
}

// 登录加载状态(可选,比如点击登录后显示加载)
class LoginLoading extends LoginState {}

// 登录成功状态(可选)
class LoginSuccess extends LoginState {}

第四步:实现LoginBloc

这部分是核心,处理事件、验证规则,输出新状态:

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

// 导入上面定义的事件和状态
import 'login_events.dart';
import 'login_states.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc() : super(LoginInitial()) {
    on<EmailChanged>(_onEmailChanged);
    on<PasswordChanged>(_onPasswordChanged);
    on<LoginSubmitted>(_onLoginSubmitted);
  }

  // 处理邮箱变化事件
  void _onEmailChanged(EmailChanged event, Emitter<LoginState> emit) {
    // 获取当前状态,如果是初始状态就初始化空值
    final currentState = state is LoginInputState ? state as LoginInputState : null;
    String email = event.email;
    String? emailError;

    // 邮箱验证规则:包含@符号
    if (!email.contains('@')) {
      emailError = '请输入有效的邮箱地址';
    }

    // 计算当前表单是否有效
    bool isFormValid = emailError == null && (currentState?.passwordError == null);

    // 输出新状态
    emit(LoginInputState(
      email: email,
      password: currentState?.password ?? '',
      emailError: emailError,
      passwordError: currentState?.passwordError,
      isFormValid: isFormValid,
    ));
  }

  // 处理密码变化事件
  void _onPasswordChanged(PasswordChanged event, Emitter<LoginState> emit) {
    final currentState = state is LoginInputState ? state as LoginInputState : null;
    String password = event.password;
    String? passwordError;

    // 密码验证规则:长度大于4位
    if (password.length <= 4) {
      passwordError = '密码长度必须大于4位';
    }

    // 计算当前表单是否有效
    bool isFormValid = passwordError == null && (currentState?.emailError == null);

    // 输出新状态
    emit(LoginInputState(
      email: currentState?.email ?? '',
      password: password,
      emailError: currentState?.emailError,
      passwordError: passwordError,
      isFormValid: isFormValid,
    ));
  }

  // 处理登录提交事件
  void _onLoginSubmitted(LoginSubmitted event, Emitter<LoginState> emit) async {
    // 先输出加载状态
    emit(LoginLoading());

    // 这里写实际的登录逻辑,比如调用API
    // await Future.delayed(Duration(seconds: 2)); // 模拟网络请求

    // 登录成功后输出成功状态,或者失败状态(可以扩展)
    // emit(LoginSuccess());
    
    // 示例里暂时回到输入状态
    final currentState = state is LoginInputState ? state as LoginInputState : LoginInputState(email: '', password: '', isFormValid: false);
    emit(currentState);
  }
}

第五步:UI层实现(对应你的LoginScreen)

用BlocProvider提供Bloc实例,用BlocBuilder监听状态变化,实时更新UI:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// 导入上面的Bloc、事件、状态
import 'login_bloc.dart';
import 'login_events.dart';
import 'login_states.dart';

class LoginScreen extends StatelessWidget {
  const LoginScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => LoginBloc(),
      child: Scaffold(
        appBar: AppBar(title: const Text('登录')),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [
              _EmailField(),
              SizedBox(height: 16),
              _PasswordField(),
              SizedBox(height: 24),
              _SubmitButton(),
            ],
          ),
        ),
      ),
    );
  }
}

// 邮箱输入框
class _EmailField extends StatelessWidget {
  const _EmailField({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState>(
      builder: (context, state) {
        String? errorText;
        if (state is LoginInputState) {
          errorText = state.emailError;
        }
        return TextField(
          keyboardType: TextInputType.emailAddress,
          decoration: InputDecoration(
            hintText: 'you@example.com',
            labelText: '邮箱地址',
            errorText: errorText,
            border: const OutlineInputBorder(),
          ),
          onChanged: (email) {
            context.read<LoginBloc>().add(EmailChanged(email: email));
          },
        );
      },
    );
  }
}

// 密码输入框
class _PasswordField extends StatelessWidget {
  const _PasswordField({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState>(
      builder: (context, state) {
        String? errorText;
        if (state is LoginInputState) {
          errorText = state.passwordError;
        }
        return TextField(
          obscureText: true,
          decoration: InputDecoration(
            labelText: '密码',
            errorText: errorText,
            border: const OutlineInputBorder(),
          ),
          onChanged: (password) {
            context.read<LoginBloc>().add(PasswordChanged(password: password));
          },
        );
      },
    );
  }
}

// 提交按钮(核心:只有表单验证通过才激活)
class _SubmitButton extends StatelessWidget {
  const _SubmitButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginBloc, LoginState>(
      builder: (context, state) {
        bool isEnabled = false;
        // 如果是加载状态,按钮不可用
        if (state is LoginLoading) {
          isEnabled = false;
        }
        // 如果是输入状态,根据isFormValid判断
        else if (state is LoginInputState) {
          isEnabled = state.isFormValid;
        }

        return ElevatedButton(
          onPressed: isEnabled
              ? () {
                  context.read<LoginBloc>().add(LoginSubmitted());
                }
              : null, // 不可用时设置为null
          style: ElevatedButton.styleFrom(
            minimumSize: const Size(double.infinity, 48),
            backgroundColor: isEnabled ? Colors.blue : Colors.grey,
          ),
          child: state is LoginLoading
              ? const CircularProgressIndicator(color: Colors.white)
              : const Text('登录', style: TextStyle(color: Colors.white)),
        );
      },
    );
  }
}

关键细节解释

  1. 状态管理:用LoginInputState统一管理所有表单状态,避免了自定义BLoC里多个Stream的混乱,所有状态变化都通过Bloc统一输出。
  2. 表单验证逻辑:每次输入变化时,都会重新计算isFormValid字段(邮箱无错误 + 密码无错误),UI直接根据这个字段控制按钮是否可用。
  3. 版本适配:用了flutter_bloc v6+的on<Event>语法替代旧版的mapEventToState,这是v6之后的新写法,也是现在的标准用法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 19:57:46