Я пытаюсь создать активность входа в систему, используя Blo c, с помощью учебных пособий, доступных по адресу https://bloclibrary.dev/. Я успешно объединил валидацию формы и поток входа в рабочее решение, но при добавлении кнопки для переключения видимости пароля дела пошли не так, как надо.
Я решил, что я буду придерживаться того же формата, что и проверки и вход в систему состояние имело (виджет onPressed запускает событие, blo c обрабатывает его и изменяет состояние для обновления представления), но поскольку состояния являются взаимоисключающими, переключение видимости пароля приводит к исчезновению другой информации (например, ошибок проверки или индикатора загрузки), потому что состояние, которое им требуется для отображения, больше не является активным.
Я предполагаю, что одним из способов избежать этого является наличие отдельного блока Blo c для обработки только переключения пароля, но я думаю, это предполагает вложение второго BlocBuilder, на мой взгляд, не говоря уже о реализации другого набора состояний Bloc + Events +, который звучит так, как будто это может затруднить понимание и навигацию кода по мере усложнения. Это как Blo c предназначен для использования, или есть более чистый подход, который лучше работает здесь, чтобы избежать этого?
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 }';
}