跳到内容

编码 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: StatefulWorkflowWorkflow

在 Kotlin 中,通常将类型分为两部分:用于公共 API 的接口,以及用于私有实现的类。Workflow 库定义了一个 Workflow 接口,需要引用特定 Workflow 接口的代码应将其用作属性和参数的类型。Workflow 接口包含一个方法,它只返回一个 StatefulWorkflow —— 可以将 Workflow 描述为“任何可以表示为 StatefulWorkflow 的事物”。

该库还定义了两个抽象类,它们定义了 workflow 的契约,应被子类化以实现您的 workflow

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 只是一个具有 VoidUnit 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 + 子级 Renderings) -> Rendering。虽然 workflow 的内部状态可能包含更详细或更全面的状态,但 Rendering(外部状态)是一种在 workflow 外部有用的类型。由于 workflow 的 render 方法可能因各种原因被基础设施调用,因此在渲染时不要执行副作用非常重要——render 方法必须是幂等的。基于事件的副作用应使用 Actions,基于状态的副作用应使用 Workers。

在构建交互式应用程序时,Rendering 类型通常(但并非总是)是一个视图模型,它将驱动 UI 层。

Workflows 可以响应 UI 事件

作为最后一个参数传递给 renderRenderContext 提供了一些有用的工具来协助创建 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 的 FlowableObservables 转换为 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 库无需支持它。