Flutter BlocListener executed only once even after event gets re-fired
我正在 Flutter 中实现 Reso Coder 的简洁架构。我按照他的指南将项目划分为层并使用依赖注入。在其中一种情况下,我希望有以下场景:管理员用户登录,在其主屏幕上查看数据,对其进行编辑,然后按一个按钮,将数据保存到本地数据库 (sqflite)。保存数据后,我想显示一个带有某种文本的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | class AdministratorPage extends StatefulWidget { @override _AdministratorPageState createState() => _AdministratorPageState(); } class _AdministratorPageState extends State<AdministratorPage> { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).backgroundColor, centerTitle: true, leading: Container(), title: Text(AppLocalizations.of(context).translate('adminHomeScreen')), ), body: SingleChildScrollView( child: buildBody(context), ), ); } BlocProvider<SettingsBloc> buildBody(BuildContext context) { return BlocProvider( create: (_) => serviceLocator<SettingsBloc>(), child: BlocListener<SettingsBloc, SettingsState>( listener: (context, state) { if (state is SettingsUpdatedState) { Scaffold.of(context).showSnackBar( SnackBar( content: Text( AppLocalizations.of(context).translate('settingsUpdated')), backgroundColor: Colors.blue, ), ); } }, child: Column( children: <Widget>[ SizedBox( height: 20.0, ), AdministratorInput(), SizedBox( width: double.infinity, child: RaisedButton( child: Text('LOG OUT'), onPressed: () { serviceLocator<AuthenticationBloc>().add(LoggedOutEvent()); Routes.sailor(Routes.loginScreen); }, ), ), ], ), ), ); } } |
这是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | class AdministratorInput extends StatefulWidget { @override _AdministratorInputState createState() => _AdministratorInputState(); } class _AdministratorInputState extends State<AdministratorInput> { String serverAddress; String daysBack; final serverAddressController = TextEditingController(); final daysBackController = TextEditingController(); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(10.0), child: BlocBuilder<SettingsBloc, SettingsState>( builder: (context, state) { if (state is SettingsInitialState) { BlocProvider.of<SettingsBloc>(context) .add(SettingsPageLoadedEvent()); } else if (state is SettingsFetchedState) { serverAddressController.text = serverAddress = state.settings.serverAddress; daysBackController.text = daysBack = state.settings.daysBack.toString(); } return Column( children: <Widget>[ Container( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text(AppLocalizations.of(context) .translate('serverAddress')), ], ), ), Container( height: 40.0, child: TextField( controller: serverAddressController, decoration: InputDecoration( border: OutlineInputBorder(), ), onChanged: (value) { serverAddress = value; }, ), ), SizedBox( height: 5.0, ), // Days Back Text Field Container( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text(AppLocalizations.of(context).translate('daysBack')), ], ), ), Container( height: 40.0, child: TextField( controller: daysBackController, decoration: InputDecoration( border: OutlineInputBorder(), ), onChanged: (value) { daysBack = value; }, ), ), SizedBox( width: double.infinity, child: RaisedButton( child: Text('SAVE CHANGES'), onPressed: updatePressed, ), ), SizedBox( width: double.infinity, child: RaisedButton( child: Text('REFRESH'), onPressed: refreshPressed, ), ), ], ); }, ), ), ); } void updatePressed() { BlocProvider.of<SettingsBloc>(context).add( SettingsUpdateButtonPressedEvent( settings: SettingsAggregate( serverAddress: serverAddress, daysBack: int.parse(daysBack), ), ), ); } void refreshPressed() { BlocProvider.of<SettingsBloc>(context).add( SettingsRefreshButtonPressedEvent(), ); } } |
SettingsBloc 是一个标准的 bloc 模式,带有事件和状态以及一个映射器方法。它是使用
1 2 3 4 5 6 7 | serviceLocator.registerFactory( () => SettingsBloc( pullUsersFromServerCommand: serviceLocator(), getSettingsQuery: serviceLocator(), updateSettingsCommand: serviceLocator(), ), ); |
命令的所有实例和对 bloc 构造函数的查询都以相同的方式正确实例化。
这里是集团:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | class SettingsBloc extends Bloc<SettingsEvent, SettingsState> { final PullUsersFromServerCommand pullUsersFromServerCommand; final UpdateSettingsCommand updateSettingsCommand; final GetSettingsQuery getSettingsQuery; SettingsBloc({ @required PullUsersFromServerCommand pullUsersFromServerCommand, @required UpdateSettingsCommand updateSettingsCommand, @required GetSettingsQuery getSettingsQuery, }) : assert(pullUsersFromServerCommand != null), assert(updateSettingsCommand != null), assert(getSettingsQuery != null), pullUsersFromServerCommand = pullUsersFromServerCommand, updateSettingsCommand = updateSettingsCommand, getSettingsQuery = getSettingsQuery; @override SettingsState get initialState => SettingsInitialState(); @override Stream<SettingsState> mapEventToState(SettingsEvent event) async* { if (event is SettingsPageLoadedEvent) { final getSettingsEither = await getSettingsQuery(NoQueryParams()); yield* getSettingsEither.fold((failure) async* { yield SettingsFetchedFailureState(error:"settingsDatabaseError"); }, (result) async* { if (result != null) { yield SettingsFetchedState(settings: result); } else { yield SettingsFetchedFailureState( error:"settingsFetchFromDatabaseError"); } }); } else if (event is SettingsUpdateButtonPressedEvent) { final updateSettingsEither = await updateSettingsCommand( UpdateSettingsParams(settingsAggregate: event.settings)); yield* updateSettingsEither.fold((failure) async* { yield SettingsUpdatedFailureState(error:"settingsDatabaseError"); }, (result) async* { if (result != null) { yield SettingsUpdatedState(); } else { yield SettingsUpdatedFailureState( error:"settingsUpdateToDatabaseError"); } }); } else if (event is SettingsRefreshButtonPressedEvent) { final pullUsersFromServerEither = await pullUsersFromServerCommand(NoCommandParams()); yield* pullUsersFromServerEither.fold((failure) async* { yield SettingsRefreshedFailureState( error:"settingsRefreshDatabaseError"); }, (result) async* { if (result != null) { yield SettingsUpdatedState(); } else { yield SettingsRefreshedFailureState(error:"settingsRefreshedError"); } }); } } } |
当我第一次进入这个屏幕时,一切都很完美。数据是从数据库中获取的,加载到屏幕上,如果我更改它并按 SAVE,它会显示
我应该如何实现我想要的行为?
编辑:
我在登录用户的应用程序中添加了我之前使用的另一个块。登录页面使用了该块,并且在错误的用户名或密码时,我显示了一个快餐栏,清除输入字段并让页面准备好更多。如果我使用错误的凭据重试,我可以再次看到小吃栏。
这里是登录块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class LoginBloc extends Bloc<LoginEvent, LoginState> { final AuthenticateUserCommand authenticateUserCommand; final AuthenticationBloc authenticationBloc; LoginBloc({ @required AuthenticateUserCommand authenticateUserCommand, @required AuthenticationBloc authenticationBloc, }) : assert(authenticateUserCommand != null), assert(authenticationBloc != null), authenticateUserCommand = authenticateUserCommand, authenticationBloc = authenticationBloc; @override LoginState get initialState => LoginInitialState(); @override Stream<LoginState> mapEventToState(LoginEvent event) async* { if (event is LoginButtonPressedEvent) { yield LoginLoadingState(); final authenticateUserEither = await authenticateUserCommand( AuthenticateUserParams( username: event.username, password: event.password)); yield* authenticateUserEither.fold((failure) async* { yield LoginFailureState(error:"loginDatabaseError"); }, (result) async* { if (result != null) { authenticationBloc.add(LoggedInEvent(token: result)); yield LoginLoggedInState(result); } else { yield LoginFailureState(error:"loginUsernamePasswordError"); } }); } } } |
这里的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | else if (event is SettingsUpdateButtonPressedEvent) { final updateSettingsEither = await updateSettingsCommand( UpdateSettingsParams(settingsAggregate: event.settings)); yield* updateSettingsEither.fold((failure) async* { yield SettingsUpdatedFailureState(error:"settingsDatabaseError"); }, (result) async* { if (result != null) { // // this part is the problem. yield SettingsUpdatedState(); } else { yield SettingsUpdatedFailureState( error:"settingsUpdateToDatabaseError"); } }); |
In general, you should use Equatable if you want to optimize your code to reduce the number of rebuilds. You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
来源:when-to-use-equatable
它与flutter_bloc 的工作原理是你不能产生相同的状态。是的,当您发出事件时,yield 状态之前的上述函数工作正常,但不会调用 yield 本身。
所以基本上你的集团会发生什么,
如何解决这个问题?根据我目前的知识,我没有足够的信心给出建议,所以不妨试试报价中所说的
编辑:
LoginBloc 之所以起作用,仅仅是因为它为每个事件产生不同的状态。我认为您没有注意到,但它会在产生 LoginLoggedInState(result) 或 LoginFailureState(error:"loginUsernamePasswordError")
之前产生 LoginLoadingState()
@Federick Jonathan 已经对这个问题给出了足够的解释,但我想在此做插件。
第一件事:
这是
让我们讨论所有可能的解决方案。
从 bloc 中删除
为状态定义
参考以下代码
1 2 3 | yield StartDataUpdate(); //Here... Please specified data changes related to operation. yield EndDataUpdate(); |
1 2 3 4 5 | Stream<ReportsState> setupState({required ReportsState state}) async* { yield StartReportsState(); yield state; yield EndReportsState(); } |
使用:
1 | yield* setupState( state: NavigationState() ); |