Flutter Bloc 冲突状态

Posted

技术标签:

【中文标题】Flutter Bloc 冲突状态【英文标题】:Flutter Bloc conflicting states 【发布时间】:2020-07-09 08:24:50 【问题描述】:

在https://bloclibrary.dev/ 提供的教程的帮助下,我正在尝试使用 Bloc 构建登录活动。我已经成功地将表单验证和登录流程结合到一个工作解决方案中,但是当添加一个按钮来切换密码可见性时,事情发生了混乱。

我想我会遵循与验证和登录状态相同的格式(小部件的 onPressed 触发事件,块处理它并更改状态以更新视图),但由于状态是互斥的,切换密码可见性会导致其他信息(如验证错误或加载指示器)消失,因为它们需要显示的状态不再是活动状态。

我认为避免这种情况的一种方法是使用单独的 Bloc 来处理密码切换,但我认为这涉及在我看来嵌套第二个 BlocBuilder,更不用说实现另一组 Bloc+Events+States,这听起来随着事情变得越来越复杂,这可能会使代码更难理解/导航。这是 Bloc 的使用方式,还是有一种更简洁的方法可以更好地避免这种情况?

class LoginForm extends StatefulWidget 
  @override
  State<LoginForm> createState() => _LoginFormState();


class _LoginFormState extends State<LoginForm> 
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) 

    _onLoginButtonPressed() 
      BlocProvider.of<LoginBloc>(context).add(
        LoginButtonPressed(
          username: _usernameController.text,
          password: _passwordController.text,
        ),
      );
    

    _onShowPasswordButtonPressed() 
      BlocProvider.of<LoginBloc>(context).add(
        LoginShowPasswordButtonPressed(),
      );
    

    return BlocListener<LoginBloc, LoginState>(
      listener: (context, state) 
        if (state is LoginFailure) 
          Scaffold.of(context).showSnackBar(
            SnackBar(
              content: Text('$state.error'),
              backgroundColor: Colors.red,
            ),
          );
        
      ,
      child: BlocBuilder<LoginBloc, LoginState>(
        builder: (context, state) 
          return Form(
            child: Padding(
              padding: const EdgeInsets.all(32.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  TextFormField(
                    decoration: InputDecoration(labelText: 'Username', prefixIcon: Icon(Icons.person)),
                    controller: _usernameController,
                    autovalidate: true,
                    validator: (_) 
                      return state is LoginValidationError ? state.usernameError : null;
                    ,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                      labelText: 'Password',
                      prefixIcon: Icon(Icons.lock_outline),
                      suffixIcon: IconButton(
                        icon: Icon(
                          state is! DisplayPassword ? Icons.visibility : Icons.visibility_off,
                          color: ColorUtils.primaryColor,
                        ),
                        onPressed: () 
                          _onShowPasswordButtonPressed();
                        ,
                      ),
                    ),
                    controller: _passwordController,
                    obscureText: state is! DisplayPassword ? true : false,
                    autovalidate: true,
                    validator: (_) 
                      return state is LoginValidationError ? state.passwordError : null;
                    ,
                  ),
                  Container(height: 30),
                  ButtonTheme(
                    minWidth: double.infinity,
                    height: 50,
                    child: RaisedButton(
                      color: ColorUtils.primaryColor,
                      textColor: Colors.white,
                      onPressed: state is! LoginLoading ? _onLoginButtonPressed : null,
                      child: Text('LOGIN'),
                    ),
                  ),
                  Container(
                    child: state is LoginLoading
                      ? CircularProgressIndicator()
                      : null,
                  ),
                ],
              ),
            ),
          );
        ,
      ),
    );
  

class LoginBloc extends Bloc<LoginEvent, LoginState> 
  final UserRepository userRepository;
  final AuthenticationBloc authenticationBloc;
  bool isShowingPassword = false;

  LoginBloc(
    @required this.userRepository,
    @required this.authenticationBloc,
  )  : assert(userRepository != null),
      assert(authenticationBloc != null);

  LoginState get initialState => LoginInitial();

  @override
  Stream<LoginState> mapEventToState(LoginEvent event) async* 

    if (event is LoginShowPasswordButtonPressed) 
      isShowingPassword = !isShowingPassword;
      yield isShowingPassword ?  DisplayPassword() : LoginInitial();
    

    if (event is LoginButtonPressed) 
      if (!_isUsernameValid(event.username) || !_isPasswordValid(event.password)) 
        yield LoginValidationError(
          usernameError: _isUsernameValid(event.username) ? null : "(test) validation failed",
          passwordError: _isPasswordValid(event.password) ? null : "(test) validation failed",
        );  //TODO update this so fields are validated for multiple conditions (field is required, minimum char size, etc) and the appropriate one is shown to user
      
      else 
        yield LoginLoading();

        final response = await userRepository.authenticate(
          username: event.username,
          password: event.password,
        );

        if (response.ok != null) 
          authenticationBloc.add(LoggedIn(user: response.ok));
        
        else 
          yield LoginFailure(error: response.error.message);
        
      
    
  

  bool _isUsernameValid(String username) 
    return username.length >= 4;
  

  bool _isPasswordValid(String password) 
    return password.length >= 4;
  

abstract class LoginEvent extends Equatable 
  const LoginEvent();

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


class LoginButtonPressed extends LoginEvent 
  final String username;
  final String password;

  const LoginButtonPressed(
    @required this.username,
    @required this.password,
  );

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

  @override
  String toString() =>
    'LoginButtonPressed  username: $username, password: $password ';


class LoginShowPasswordButtonPressed extends LoginEvent 
abstract class LoginState extends Equatable 
  const LoginState();

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


class LoginInitial extends LoginState 

class LoginLoading extends LoginState 

class LoginValidationError extends LoginState 
  final String usernameError;
  final String passwordError;

  const LoginValidationError(@required this.usernameError, @required this.passwordError);

  @override
  List<Object> get props => [usernameError, passwordError];


class DisplayPassword extends LoginState 

class LoginFailure extends LoginState 
  final String error;

  const LoginFailure(@required this.error);

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

  @override
  String toString() => 'LoginFailure  error: $error ';

【问题讨论】:

【参考方案1】:

是的,你不应该有这个。 // class DisplayPassword extends LoginState

是的,如果您想使用纯 BLoC,那么使用 imo 是正确的方法。在这种情况下,因为您想要保持的唯一状态是单个 bool 值,所以您可以使用 BLoC 结构的更简单的方法。我的意思是,您不需要制作完整的集合、事件类、状态类、bloc 类,而只需制作 bloc 类。最重要的是,您可以将 bloc 文件夹分为两种。

bloc
 - full
    - login_bloc.dart
    - login_event.dart
    - login_state.dart
 - single
    - password_visibility_bloc.dart
class PasswordVisibilityBloc extends Bloc<bool, bool> 
  @override
  bool get initialState => false;

  @override
  Stream<bool> mapEventToState(
    bool event,
  ) async* 
    yield !event;
  

【讨论】:

以上是关于Flutter Bloc 冲突状态的主要内容,如果未能解决你的问题,请参考以下文章

Flutter - flutter_bloc状态管理

Flutter 从其他 Bloc 监听 Bloc 状态

Flutter 状态管理之Bloc上

Flutter 状态管理之Bloc上

Flutter 状态管理之Bloc上

Flutter 状态管理(BloC):无状态与有状态小部件