0

在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 }';
}
4

1 回答 1

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;
  }
}
于 2020-03-28T19:34:16.487 回答