【问题标题】:Flutter BlocListener executed only once even after event gets re-fired即使在事件重新触发后 Flutter BlocListener 也只执行一次
【发布时间】:2020-06-21 02:40:46
【问题描述】:

我正在颤振中实现Reso Coder's clean architecture。我按照他的指南将项目划分为层并使用依赖注入。在其中一种情况下,我希望有以下场景:管理员用户登录,在其主屏幕上查看数据,对其进行编辑,然后按一个按钮,将数据保存到本地数据库(sqflite)。保存数据后,我想显示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 小部件:

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 模式,带有事件和状态以及一个映射器方法。它是使用get_it 包注入的。下面是实例化的方式:

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

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

这是集团:

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,它会显示snackbar。我的问题是如果我想在停留在该屏幕上时再次编辑数据。我再次编辑它,因此触发更改事件,块获取它,调用下面的正确命令并将数据保存在数据库中。然后更改 bloc 的状态以试图告诉 UI,“嘿,我有一个新状态,使用它”。但是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");
        }
      });
    }
  }
}

这里的EventState 类扩展了Equatable。而且由于它按照预期工作,我在“设置”页面中以相同的方式进行了操作(失败的地方)。在 UI 中,我可以根据需要多次调用LoginButtonPressedEvent,并分别调用BlocListener

【问题讨论】:

  • 你的 bloc 文件在哪里,这是最重要的部分
  • 是的!抱歉 - 我已经添加了。

标签: flutter dependency-injection bloc


【解决方案1】:
    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");
        }
      });

一般来说,如果您想优化代码以减少重建次数,您应该使用 Equatable。如果您希望背靠背的相同状态触发多个转换,则不应使用 Equatable。

来源:when-to-use-equatable

它与 flutter_bloc 的工作原理是你不能产生相同的状态。是的,当您发出事件时,yield 状态之前的上述函数工作正常,但不会调用 yield 本身。

所以基本上你的集团会发生什么,

  1. 当前状态为 SettingsFetchedState(settings: result)
  2. 您发出 SettingsUpdateButtonPressedEvent()
  3. Bloc yield SettingsUpdatedState()
  4. 状态从 SettingsFetchedState(settings: result) 更改为 SettingsUpdatedState()
  5. 当前状态为 SettingsUpdatedState()
  6. BlocListener 监听从 SettingsFetchedState(settings: result) 到 SettingsUpdatedState() 的状态变化
  7. 您发出 SettingsUpdateButtonPressedEvent()
  8. Bloc 不会产生 SettingsUpdatedState(),它会被忽略,因为相等比较返回 true)
  9. BlocListener 什么都不做,因为没有状态变化。

如何解决这个问题?根据我目前的知识,我没有足够的信心给出建议,所以不妨试试引用的内容You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.

编辑:

LoginBloc 之所以起作用,仅仅是因为它为每个事件产生不同的状态。我认为您没有注意到,但它会在产生 LoginLoggedInState(result) 或 LoginFailureState(error: "loginUsernamePasswordError") 之前产生 LoginLoadingState()

  1. 当前状态为 LoginInitialState()
  2. 发射事件
  3. 产生 LoginLoadingState()
  4. 状态从 LoginInitialState() 更改为 LoginLoadingState()
  5. 产生 LoginLoggedInState() 或 LoginFailurestate()
  6. 状态从 LoginLoadingState() 更改为 LoginLoggedInState() 或 LoginFailurestate()
  7. 为每个事件返回第 2 步

【讨论】:

  • 这是一个非常好的方向!我试过了,它奏效了!但在我将您的答案标记为真实之前,我想分享更多代码并扩展我的问题。我有另一个页面(登录页面),我做了完全相同的操作:LoginBloc,其状态扩展为Equatable,并且操作相似 - 按 LOGIN,如果不匹配,则显示 ERROR 快餐栏并保持在同一页面上。如果再次进行错误尝试,请再次显示小吃吧。并以与我在上面使用设置相同的方式进行操作 - 它有效。这就是为什么当它第一次不起作用时我很困惑。
  • 我编辑了原来的帖子,再添加一个块来看看
  • 你的意思是......因为我在交替他们?加载 -> 错误 -> 加载 -> 错误...而在第二种情况下,我只“拍摄”完成 -> 完成 -> 完成...?
  • 是的,基本上就是这样
  • 非常感谢!这就解释了一切!
【解决方案2】:

@Federick Jonathan 已经对问题给出了足够的解释,但我想在此做插件。

第一件事: 这是Equatable 的标准行为,当状态发生变化时会调用事件监听器。如果你 yield 每次都处于相同的状态,那么什么都不会发生。

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

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

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

参考以下代码

yield StartDataUpdate();
//Here... Please specified data changes related to operation.
yield EndDataUpdate();
  Stream<ReportsState> setupState({required ReportsState state}) async* {
    yield StartReportsState();
    yield state;
    yield EndReportsState();
  }

使用:

 yield* setupState( state: NavigationState() );

【讨论】:

  • 是的,那是最简单的方法,只是我使用了一个不包含任何内容的临时状态,我在产生的状态之前产生了该状态,因此您可以使用 equatble 与产生的相同状态。谢谢
  • 感谢您的评论!我能够通过创建一个除了切换状态什么都不做的状态来不断刷新状态。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-10-14
相关资源
最近更新 更多