触发并等待不同 BLoC 中的项目创建

trigger and wait for item creation in different BLoC

对于我想要实现的简单事情,我的以下方法感觉有点复杂:

我有一个由 TaskBloc 管理的 Task 列表。 UI 列出所有任务并为每个任务提供一个 execute 按钮。对于每次单击该按钮,我想创建并存储一个 Action(基本上是任务执行发生时的时间戳)并在创建操作时显示一个微调器。我有一个 ActionBloc 来管理操作(例如创建或获取每个任务的历史记录)。

我不知道如何设置 BLoC 之间的通信。


这是我目前的方法。

ActionsState 仅包含所有已存储操作的列表。

class ActionsState extends Equatable {
  final List<Action> actions;
  // ... copyWith, constructors etc.
}

Action 是一个简单的 PODO class 持有 idtimestamp.

ActionsBloc 能够创建 Action 以响应它的 ActionCreationStarted 事件(持有 int taskId)。由于 Action 创建是在异步隔离中执行的,因此在请求完成后,隔离还会添加事件 ActionCreationSucceededActionCreationFailed。两者都包含已创建或创建失败的 Action

TaskState:

class TaskState extends Equatable {
  final Map<int, Task> tasks;
  // ... copyWith, constructors, etc.

我在Task模型中添加了一个executeStatus来跟踪任务列表中创建请求的状态(特定任务不能并行执行多次,只能顺序执行而不同的任务可以并行执行):

enum Status { initial, loading, success, error }

class Task extends Equatable {
  final int id;
  final Status executeStatus;
  // ...
}

我为 TaskBloc 添加了事件:

class TaskExecutionStarted extends TaskEvent {
  final int taskId;
  // ...
}
class TaskExecutionSucceeded extends TaskEvent {
  final int taskId;
  // ...
}
class TaskExecutionFailed extends TaskEvent {
  final int taskId;
  // ...
}

TaskBloc 中,我为新事件实现了 mapEventToState 以根据事件设置任务状态,例如TaskExecutionStarted:

Stream<TaskState> mapEventToState(TaskEvent event) async* {
  // ...
  if (event is TaskExecutionStarted) {
    final taskId = event.taskId;
    Task task = state.tasks[taskId]!;
    yield state.copyWith(
      tasks: {
        ...state.tasks,
        taskId: task.copyWith(executeStatus: Status.loading),
      },
    );
  }
  // ...
}

到目前为止,这使 UI 能够为每个任务显示一个旋转器,但是 ActionBloc 还不知道它应该记录一个新的 Action 用于该任务并且 TaskBloc 不知道何时停止显示微调器。


问题

现在我迷路的部分是我需要实际触发 ActionBloc 来创建一个动作并 得到一个 TaskExecutionSucceeded (或 ...Failed)之后的事件。我考虑过在 ActionsBloc 上使用监听器,但它只提供状态而不是 ActionsBloc 的事件(我需要对 ActionCreationSucceeded 事件做出反应,但监听事件其他集团的感觉就像一个反模式(?!),我什至不知道如何设置它)。

问题的核心是,我可能会监听 ActionsBloc 状态,但我不知道如何区分我需要触发 TaskExecutionSucceeded 的状态的哪些动作事件。

无论如何,我给了 TaskBloc 一个参考 ActionsBloc:

class TaskBloc extends Bloc<TaskEvent, TaskState> {
  final ActionsBloc actionsBloc;
  late final StreamSubscription actionsSubscription;
  // ...
  TaskBloc({
    // ...
    required this.actionsBloc,
  }) : super(TaskState.initial()) {
    actionsSubscription = actionsBloc.listen((state) {
      /* ... ??? ... Here I don't know how to distinguish for which actions of the state
           I would somehow need to trigger a `TaskExecutionSucceeded` event. */
    });
  };
  // ...
}

为了完整起见,触发创建 Action 很简单,只需将相应的事件添加到 ActionBloc 作为对 TaskExecutionStarted 的响应:

Stream<TaskState> mapEventToState(TaskEvent event) async* {
  // ...
  // ... set executeStatus: Status.loading as shown above ...
  // trigger creating a new action
  actionsBloc.add(ActionCreationStarted(taskId: taskId));
  // ...

当然,我的目标是清楚地分离关注点、单一事实来源和其他 accidential complexity regarding app state structure 的潜在来源 - 但 总的来说,这种方法(在工作之前仍然说问题未解决)感觉复杂的方法只是存储任务的每个动作的时间戳并跟踪动作创建请求。

感谢您到目前为止的阅读 (!),并且我很高兴看到针对该用例的干净架构的提示。

所以我们最终做的是:

ActionsState中引入一个lastCreatedState,代表最后创建的动作的状态。

我们不是一直监听 ActionsBloc,而是在任务执行时临时监听它的状态,并记住每个事件的监听器。

一旦我们在 ActionsBloc lastCreatedState 状态发生变化,表明我们的任务成功或失败,我们将删除侦听器并对其做出反应。

大致如下:

/// When a task is executed, we trigger an action creation and wait for the
/// [ActionsBloc] to signal success or failure for that task.
/// The mapping maps taskId => subscription.
final Map<int, StreamSubscription> _actionsSubscription = {};

Stream<TaskState> mapEventToState(TaskEvent event) async* {

  // ...

  // trigger creation of an action
  actionsBloc.add(ActionCreationStarted(taskId: taskId));
  // listen to the result
  // remove any previous listeners
  if (_actionsSubscription[taskId] != null) {
    await _actionsSubscription[taskId]!.cancel();
  }
  StreamSubscription<ActionsState>? listener;
  listener = actionsBloc.stream.listen((state) async {
    final status = state.lastCreatedState?.status;
    final doneTaskId = state.lastCreatedState?.action?.taskId;
    if (doneTaskId == taskId &&
        (status == Status.success || status == Status.error)) {
      await listener?.cancel(); // stop listening
      add(TaskExecutionDone(taskId: taskId, status: status!));
    }
  });
  _actionsSubscription[taskId] = listener;
}

@override
Future<void> close() {
  _actionsSubscription.values.forEach((s) {
    s.cancel();
  });
  return super.close();
}

它并不完美:它需要 ActionsState 的污染并且它需要 TaskBlocnot 在所有听众完成之前被释放(或者至少有其他东西可以确保状态在创建时被水化和同步)和污染的状态。

虽然内部结构稍微复杂一些,但它使事物分离并使使用 blocs 变得轻而易举。