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