Flutter BlocListener仅在事件重新触发后仅执行一次

时间:2020-03-09 08:04:57

标签: flutter dependency-injection bloc

我正在扑朔迷离地实现Reso Coder's clean architecture。我按照他的指导将项目分为多个层并使用依赖项注入。在一种情况下,我希望具有以下情形:管理员用户登录,在其主屏幕上看到数据,进行编辑,然后按一个按钮,将数据保存到本地db(sqflite)。保存数据后,我想显示一个from requests_html import HTMLSession import pyppdf.patch_pyppeteer from bs4 import BeautifulSoup url = 'https://xxxxx.com/properties/?sort=latest' session = HTMLSession() resp = session.get(link) resp.html.render() html = resp.html.html page_soup = BeautifulSoup(html, 'html.parser') containers = page_soup.find_all("div", {"class": "grid-item"}) 并带有某种文本“设置已保存!”。例如。这是我的代码(部分):

Snackbar

这是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); }, ), ), ], ), ), ); } } 小部件:

AdministratorInput

SettingsBloc是具有事件和状态以及映射器方法的标准块模式。正在使用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(), ); } } 软件包进行注入。实例化方法如下:

get_it

以相同的方式正确实例化命令的所有实例以及对bloc的构造函数的查询。

这是集团:

serviceLocator.registerFactory(
    () => SettingsBloc(
      pullUsersFromServerCommand: serviceLocator(),
      getSettingsQuery: serviceLocator(),
      updateSettingsCommand: serviceLocator(),
    ),
  );

我第一次进入此屏幕时,一切正常。从数据库中获取数据,将其加载到屏幕上,如果我更改它并按SAVE,它将显示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"); } }); } } } 。我的问题是,如果我想在停留在该屏幕上的同时再次编辑数据。我再次对其进行编辑,因此触发更改事件,块将其获取,在下面调用适当的命令并将数据保存在数据库中。然后更改块的状态,以尝试告诉UI:“嘿,我有一个新状态,请使用它”。但是snackbar再也不会被呼叫。

我应该如何实现我想要的行为?

编辑: 我在我之前登录用户的应用程序中添加了另一个我正在使用的块。登录页面利用该块,并在错误的用户名或密码后显示一个小吃栏,清除输入字段,并为该页面准备更多内容。如果我使用错误的凭据再次尝试,则可以再次看到小吃栏。

这是LoginBloc:

BlocListener

此处的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"); } }); } } } Event类扩展了State。由于它按照预期工作,因此我在“设置”页面(失败的地方)以相同的方式进行了操作。从UI中,我可以根据需要提高Equatable的次数,并分别调用LoginButtonPressedEvent

2 个答案:

答案 0 :(得分:7)

private double getDegrees(double rad)
{
     double deg = rad * Mathf.Rad2Deg;

     double degree = (-135 - deg) % 360;

     return degree < 0 ? degree + 360 : degree;
}

通常,如果要优化代码以减少重建次数,则应使用Equatable。如果您希望背对背相同状态触发多个转换,则不要使用Equatable。

来源: when-to-use-equatable

如何与flutter_bloc一起使用,是因为您无法产生相同的状态。是的,当您发出事件时,上述在yield状态之前的函数可以正常工作,但是yield本身不会被调用。

基本上,您的集团会发生什么,

  1. 当前状态为SettingsFetchedState(设置:结果)
  2. 您发出SettingsUpdateButtonPressedEvent()
  3. 批量产量SettingsUpdatedState()
  4. 状态从SettingsFetchedState(设置:结果)更改为SettingsUpdatedState()
  5. 当前状态为SettingsUpdatedState()
  6. BlocListener侦听从SettingsFetchedState(设置:结果)到SettingsUpdatedState()的状态更改
  7. 您发出SettingsUpdateButtonPressedEvent()
  8. 集团不产生SettingsUpdatedState(),它被忽略,因为相等比较返回true)
  9. BlocListener不执行任何操作,因为状态没有变化。

如何解决此问题?我没有足够的信心根据当前的知识提出建议,因此,请尝试引用引言中的 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"); } });

编辑:

LoginBloc的工作原理很简单,因为它为每个事件产生不同的状态。我认为您没有注意到,但是在产生LoginLoggedInState(result)或LoginFailureState(error:“ loginUsernamePasswordError”)之前会产生LoginLoadingState()

  1. 当前状态为LoginInitialState()
  2. 发射事件
  3. 收益率LoginLoadingState()
  4. 状态从LoginInitialState()更改为LoginLoadingState()
  5. 选择LoginLoggedInState()或LoginFailurestate()
  6. 状态从LoginLoadingState()更改为LoginLoggedInState()或LoginFailurestate()
  7. 针对每个事件返回步骤2

答案 1 :(得分:2)

@Federick Jonathan已经给出了足够的关于该问题的解释,但是我想在此添加附件。

第一件事: 这是Equatable的标准行为,状态更改发生时将调用事件侦听器。如果您每次yield处于相同状态,那么什么都不会发生。

让我们讨论了所有可能的解决方案。

  1. 从集团中删除Equatable,然后在state更改时触发每个事件。

  2. 为状态定义startend状态。例如,将第一个state创建为StartDataUpdate,然后将第二个创建为EndDataUpdate

请参考以下代码

yield StartDataUpdate();
//Here... Please specified data changes related to operation.
yield EndDataUpdate();