触发并等待不同 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 持有 id
和 timestamp
.
ActionsBloc
能够创建 Action
以响应它的 ActionCreationStarted
事件(持有 int taskId
)。由于 Action
创建是在异步隔离中执行的,因此在请求完成后,隔离还会添加事件 ActionCreationSucceeded
和 ActionCreationFailed
。两者都包含已创建或创建失败的 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
的污染并且它需要 TaskBloc
到 not 在所有听众完成之前被释放(或者至少有其他东西可以确保状态在创建时被水化和同步)和污染的状态。
虽然内部结构稍微复杂一些,但它使事物分离并使使用 blocs 变得轻而易举。
对于我想要实现的简单事情,我的以下方法感觉有点复杂:
我有一个由 TaskBloc
管理的 Task
列表。 UI 列出所有任务并为每个任务提供一个 execute
按钮。对于每次单击该按钮,我想创建并存储一个 Action
(基本上是任务执行发生时的时间戳)并在创建操作时显示一个微调器。我有一个 ActionBloc
来管理操作(例如创建或获取每个任务的历史记录)。
我不知道如何设置 BLoC 之间的通信。
这是我目前的方法。
ActionsState
仅包含所有已存储操作的列表。
class ActionsState extends Equatable {
final List<Action> actions;
// ... copyWith, constructors etc.
}
Action
是一个简单的 PODO class 持有 id
和 timestamp
.
ActionsBloc
能够创建 Action
以响应它的 ActionCreationStarted
事件(持有 int taskId
)。由于 Action
创建是在异步隔离中执行的,因此在请求完成后,隔离还会添加事件 ActionCreationSucceeded
和 ActionCreationFailed
。两者都包含已创建或创建失败的 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
的污染并且它需要 TaskBloc
到 not 在所有听众完成之前被释放(或者至少有其他东西可以确保状态在创建时被水化和同步)和污染的状态。
虽然内部结构稍微复杂一些,但它使事物分离并使使用 blocs 变得轻而易举。