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 冲突状态的主要内容,如果未能解决你的问题,请参考以下文章