编码 Workflow¶
在代码中,Workflow
是一个 Swift 协议 或 Kotlin 接口,带有 State、Rendering 和 Output 参数类型。Kotlin 接口还定义了一个 Props 类型。在 Swift 中,props 是实现 Workflow 的结构体的属性,是隐式的。
public protocol Workflow: AnyWorkflowConvertible {
associatedtype State
associatedtype Output = Never
associatedtype Rendering
func makeInitialState() -> State
func workflowDidChange(from previousWorkflow: Self, state: inout State)
func render(state: State, context: RenderContext<Self>) -> Rendering
}
abstract class StatefulWorkflow<in PropsT, StateT, out OutputT : Any, out RenderingT> :
Workflow<PropsT, OutputT, RenderingT> {
abstract fun initialState(
props: PropsT,
initialSnapshot: Snapshot?
): StateT
open fun onPropsChanged(
old: PropsT,
new: PropsT,
state: StateT
): StateT = state
abstract fun render(
props: PropsT,
state: StateT,
context: RenderContext<StateT, OutputT>
): RenderingT
abstract fun snapshotState(state: StateT): Snapshot
}
Swift: 什么是 AnyWorkflowConvertible
?
当协议具有关联的 Self
类型时,Swift 需要使用 类型擦除包装器 来存储该协议实例的引用。AnyWorkflow
就是用于 Workflow
的此类包装器。AnyWorkflowConvertible
是一个协议,它有一个返回 AnyWorkflow
的方法。它作为一个基础类型很有用,因为它允许需要类型擦除 AnyWorkflow
的任何代码直接使用 Workflow
的实例。
Kotlin: StatefulWorkflow
与 Workflow
在 Kotlin 中,通常将类型分为两部分:用于公共 API 的接口,以及用于私有实现的类。Workflow 库定义了一个 Workflow
接口,需要引用特定 Workflow
接口的代码应将其用作属性和参数的类型。Workflow
接口包含一个方法,它只返回一个 StatefulWorkflow
—— 可以将 Workflow
描述为“任何可以表示为 StatefulWorkflow
的事物”。
该库还定义了两个抽象类,它们定义了 workflow 的契约,应被子类化以实现您的 workflow
StatefulWorkflow
应该被子类化以实现具有 私有状态 的 Workflows。StatelessWorkflow
应该被子类化以实现不具有任何私有状态的 Workflows。请参阅 无状态 Workflows。
Workflows 有几个职责
Workflows 具有状态¶
Workflow 一旦启动,它始终在某个状态的上下文中运行。此状态分为两部分:私有状态(只有 Workflow 实现本身知道,由 State
类型定义)和属性(或“props”),属性由其父级传递给 Workflow(稍后会介绍更多关于分层 workflow 的内容)。
私有状态¶
每个 Workflow 实现都定义了一个 State
类型,以便在 workflow 运行时维护任何必要的状态。
例如,一个井字棋游戏可能具有这样的状态
struct State {
enum Player {
case x
case o
}
enum Space {
case unfilled
filled(Player)
}
// 3 rows * 3 columns = 9 spaces
var spaces: [Space] = Array(repeating: .unfilled, count: 9)
var currentTurn: Player = .x
}
data class State(
// 3 rows * 3 columns = 9 spaces
val spaces: List<Space> = List(9) { Unfilled },
val currentTurn: Player = X
) {
enum class Player {
X, O
}
sealed class Space {
object Unfilled : Space()
data class Filled(val player: Player) : Space()
}
}
workflow 首次启动时,会查询其初始状态值。从那时起,workflow 可能会因来自各种来源的事件发生而推进到新状态(这将在下面介绍)。
无状态 Workflows
如果 workflow 没有私有状态,它通常被称为“无状态 workflow”。无状态 Workflow 只是一个具有 Void
或 Unit
State
类型的 Workflow。请参阅 更多。
公共 Props¶
每个 Workflow 实现还定义了传递给它的数据。Workflow 本身无法修改此状态,但它可能在渲染过程(render passes)之间发生变化。这种公共状态称为 Props
。
在 Swift 中,props 简单地定义为实现 Workflow 结构体的属性。在 Kotlin 中,Workflow
接口定义了一个单独的 PropsT
类型参数。(这个额外的类型参数是必要的,因为 Kotlin 缺乏 Swift workflow 的 workflowDidChange
方法所依赖的 Self
类型。)
TK
data class Props(
val playerXName: String
val playerOName: String
)
Workflows 通过 WorkflowAction
推进¶
任何时候发生应该推进 workflow 的事情——UI 事件、网络响应、子级的输出事件——都会使用 actions 来执行更新。例如,workflow 可以通过将这些事件映射到符合/实现 WorkflowAction
的类型来响应 UI 事件。这些类型实现了通过以下方式推进 workflow 的逻辑:
- 推进到新状态
- (可选地) 向上层级发出输出事件。
WorkflowAction
通常定义为带有关联类型的枚举(Swift)或密封类(Kotlin),并且可以包含来自事件的数据——例如,列表中被点击项的 ID。
将按钮点击记录到分析框架等副作用通常也在 actions 中执行。
如果您熟悉 React/Redux,那么 WorkflowAction
本质上就是 reducer。
Workflows 可以向其父级向上层级发出输出事件¶
当 workflow 被 action 推进时,可以将一个可选的输出事件向上发送到 workflow 层级结构。这是 workflow 通知其父级有事情发生的机会(也是父级通过分派自己的 action 来响应该事件的机会,只要输出事件被发出,就会向上遍历整个树)。
Workflows 通过 Rendering
生成其状态的外部表示¶
workflow 在启动后或状态转换发生后,其 render
方法将被调用。此方法负责创建并返回一个 Rendering
类型的值。可以将 Rendering
视为 workflow 的“外部发布状态”,将 render
函数视为一个映射关系 (Props
+ State
+ 子级 Rendering
s) -> Rendering
。虽然 workflow 的内部状态可能包含更详细或更全面的状态,但 Rendering
(外部状态)是一种在 workflow 外部有用的类型。由于 workflow 的 render 方法可能因各种原因被基础设施调用,因此在渲染时不要执行副作用非常重要——render 方法必须是幂等的。基于事件的副作用应使用 Actions,基于状态的副作用应使用 Workers。
在构建交互式应用程序时,Rendering
类型通常(但并非总是)是一个视图模型,它将驱动 UI 层。
Workflows 可以响应 UI 事件¶
作为最后一个参数传递给 render
的 RenderContext
提供了一些有用的工具来协助创建 Rendering
值。
如果 workflow 正在生成视图模型,通常需要一个事件处理器来响应 UI 事件。RenderContext
提供了创建事件处理器的 API,该处理器称为 Sink
,调用时将通过向 workflow 分派 action 来推进 workflow(有关 action 的更多信息,请参阅上文)。
func render(state: State, context: RenderContext<DemoWorkflow>) -> DemoScreen {
// Create a sink of our Action type so we can send actions back to the workflow.
let sink = context.makeSink(of: Action.self)
return DemoScreen(
title: "A nice title",
onTap: { sink.send(Action.refreshButtonTapped) }
}
TK
Workflows 形成层级结构 (它们可以有子级)¶
当 workflow 生成 Rendering
值时,通常会将部分工作委托给子 workflow。这通过传递给 render
方法的 RenderContext
来完成。为了委托给子级,父级在 context 上调用 renderChild
,并将子 workflow 作为唯一参数。如果这是第一次使用该子级,基础设施将启动该子 workflow(包括初始化其初始状态);如果该子级在前一次 render
过程中也使用了,则现有子级将被更新。无论哪种方式,(由 Workflow 基础设施)将立即在子级上调用 render
,并且产生的子级 Rendering
值将返回给父级。
这使得父级可以返回复杂的 Rendering
类型(例如表示整个应用程序 UI 状态的视图模型),而无需在单个 workflow 中建模所有这些复杂性。
Workflow 标识
Workflow 基础设施会自动检测您第一次和随后最后一次请求渲染子 workflow 的时机,并将自动初始化子级并清理它。在 Swift 和 Kotlin 中,这是通过使用 workflow 的具体类型来完成的。这两种语言都使用反射进行此比较(例如,在 Kotlin 中,比较 workflow 的 KClass
)。
在同一次渲染过程(render pass)中多次渲染同类型 workflow 是错误的。由于类型用于 workflow 标识,因此子渲染 API 接受一个可选的字符串 key,以区分同类型的多个子 workflow。
Workflows 可以订阅外部事件源¶
如果 workflow 需要响应某些外部事件源(例如推送通知),workflow 可以要求 context 从 render
方法内部监听这些事件。
Swift 对比 Kotlin
在 Swift 库中,有一个特殊的 API 用于订阅热流(ReactiveSwift 中的 Signal
)。Kotlin 库没有用于订阅热流(channels)的特殊 API,但它确实有扩展方法可以将 ReceiveChannel
以及 RxJava 的 Flowable
和 Observables
转换为 Worker
。这种差异的原因仅仅是我们在生产环境中尚未使用 channels,因此我们决定保持 API 更简单。如果将来我们开始使用 channels,像 Swift 那样将订阅它们作为一级 API 可能更有意义。
Workflows 可以执行异步任务 (Workers)¶
Workers
在概念上与子 workflow 非常相似。然而,与子 workflow 不同的是,workers 没有 Rendering
类型;它们仅用于执行单个异步任务,然后将零个或多个输出事件向上发送回其父级。
有关 workers 的更多信息,请参阅下面的 Worker 部分。
Workflows 可以保存到快照并从中恢复 (仅限 Kotlin)¶
在每次渲染过程(render pass)中,都会要求每个 workflow 创建其状态的“快照”——这是 workflow State
的惰性序列化表示为一个二进制 blob。这些 Snapshot
会聚合成整个 workflow 树的一个 Snapshot
,并与根 workflow 的 Rendering
一起发出。当 workflow 运行时启动时,可以向其传递一个可选的 Snapshot
以从中恢复树。当非空时,会提取根 workflow 的快照并将其传递给根 workflow 的 initialState
。workflow 可以选择忽略该快照或使用它来恢复其 State
。在第一次渲染过程中,如果根 workflow 渲染了在快照拍摄时也在渲染的任何子级,则这些子级的快照也会从聚合中提取出来并用于初始化它们的状态。
为什么 Swift Workflows 不支持快照功能?
快照功能被内置到 Kotlin workflow 中,专门用于支持 Android 的应用程序生命周期,Android 要求应用程序在进入后台之前序列化其当前状态,以便在系统需要终止宿主进程时可以恢复。iOS 应用程序没有此要求,因此 Swift 库无需支持它。