簡體   English   中英

Flutter Bloc 沖突狀態

[英]Flutter Bloc conflicting states

我正在嘗試在https://bloclibrary.dev/ 上提供的教程的幫助下,使用 Bloc 構建登錄活動。 我已經成功地將表單驗證和登錄流程組合成一個有效的解決方案,但是當添加一個按鈕來切換密碼可見性時,事情變得一團糟。

我想我會遵循與驗證和登錄狀態相同的格式(小部件的 onPressed 觸發一個事件,bloc 處理它並更改狀態以更新視圖),但是因為狀態是相互排斥的,所以切換密碼可見性會導致其他信息(如驗證)錯誤或加載指示器)消失,因為它們需要顯示的狀態不再是活動狀態。

我認為避免這種情況的一種方法是有一個單獨的 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 }';
}

是的,你不應該有這個。 // class DisplayPassword extends LoginState {}

是的,如果你想使用純 BLoC,這是使用 imo 的正確方法。 在這種情況下,因為您想要保持的唯一狀態是單個 bool 值,所以您可以使用 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;
  }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM