跳到内容

Workflow UI

本页面概述了 Workflow UI,它是 Workflow Core 驱动 Android 和 iOS 应用的配套组件。要了解这些思想如何在代码中实现,请继续阅读编写 Workflow UI 代码

警告

本文讨论的核心 Screen 接口最近才通过 v1.8.0-beta01 版本进入 Kotlin。因此,如果您使用的是最新的非 Beta 版本,您会发现此处的代码块与您所看到的不符。

Square 正在使用 Beta 版本中引入的 Screen 机制作为其 Android 应用套件的核心,我们预计 Beta 阶段会很短。Swift Screen 协议等已经稳定使用了多年。

什么是 Screen?

大多数 Workflow 实现会生成可以作为视图模型的 struct / data class rendering。这种 rendering 提供了足够的数据来绘制完整的 UI,包括响应 UI 事件时要调用的函数。

这些视图模型 rendering 实现了 Screen 协议 / 接口,以表明这是它们的预期用途。Workflow UI 提供的核心服务是将 Screen 类型转换为平台特定的 view 对象,并在发出新的 Screen rendering 时保持这些 view 的更新。

Screen 是连接 Workflow Core 和 Workflow UI 的关键,是 Workflow 驱动应用的基本 UI 构建块。Screen 是一个可以呈现为基本 2D UI 框的对象,例如 android.view.ViewUIViewController。Workflow UI 提供了粘合剂,允许您(在编译时!)声明 FooScreen : Screen 的实例用于驱动 FooViewControllerlayout/foo_screen.xml@Composable fun Content(FooScreen, ViewEnvironment)

为什么叫“Screen”?

我们选择“Screen”这个名字是因为“View”会与同名的 Android 和 iOS 类混淆,而且我们没想到“Box”。(似乎没有人因为 Screen 和 iOS 的 UIScreen 无关而感到困扰。)

实际上,我们之所以选择“Screen”,是因为这是我们和用户一直用来讨论我们的应用的一个模糊术语:“去设置 Screen。”“我怎么去小费 Screen?”“在平板电脑上,购物车 Screen 在主页 Screen 上方以模态显示。”很可能您理解了这些句子中的每一个。

Workflow 树、Rendering 树、View 树

在 Workflow Core 页面中,我们讨论了 Workflows 如何组合成树状结构,例如这个由三个 Workflows 驱动的电子邮件应用,它们组合生成了一个复合的 SplitScreen rendering。

Workflow schematic showing a parent EmailBrowserWorkflow assembling the renderings of its children, InboxWorkflow and MessageWorkflow, into a SplitScreen(InboxScreen, MessageScreen)

让我们看看 Workflow UI 如何将这种容器屏幕转换为容器视图

Workflow Core 运行时与原生视图系统之间的主要连接是来自根 Workflow(在本文中是 EmailBrowserWorkflow)的 Rendering 对象流。从那时起,控制流完全在视图层面。

这个过程的具体细节在 Android 和 iOS 之间有所不同,体现在命名、继承与委托等方面,主要是为了确保 API 对各自的用户来说是符合习惯用法的。尽管如此,其大体思路是相同的。(请继续阅读编写 Workflow UI 代码,深入了解平台特定的细节。)

每种 Workflow UI 都提供了两个核心的容器辅助工具,如下图所示

  • 一个“workflow 容器”,能够实例化和更新一个可以显示给定类型的 Screen 实例的视图
    • 在 iOS 中,这是 DescribedViewController
    • 对于 Android Classic,我们提供 WorkflowViewStub,它与 android.view.ViewStub 非常相似。
    • Android Jetpack Compose 代码可以调用 @Compose fun WorkflowRendering()
  • 一个“workflow 根容器”,能够接收来自 Workflow Core 运行时的 rendering 流,并将它们传递给 workflow 容器
    • iOS 的 ContainerViewController
    • Android Classic 的 WorkflowLayout
    • Android Jetpack Compose 的 @Compose fun Workflow.renderAsState()

A box labeled Runtime contains the EmailBrowserWorkflow. It slightly overlaps a larger box labeled Native view system. A line from the EmailBrowserWorkflow's Rendering port connects to a  box at the top of the Native View System, labeled Workflow root container. That Rendering, a SplitScreen(InboxScreen, MessageScreen), is passed from the Workflow root container down to a bi-part box labeled Workflow container / Custom split view. From there, InboxScreen is passed down to a similar bi-part box labeled Workflow container / Custom inbox view, and MessageScreen to Workflow container / Custom message view

当我们的示例中的运行时启动时,流程大致如下

  • EmailBrowserWorkflow 被要求提供它的第一个 Rendering,这是一个包含了 InboxScreenMessageScreenSplitScreen
  • Workflow 根容器接收到它,并将其交给它的 Workflow 容器。
    • 该容器能够解析出 SplitScreen 实例可以由关联类型 Custom split view 的视图显示。
    • 该容器构建了该视图,并将 SplitScreen 传递给它。
  • Custom split view 本身是用两个 Workflow 容器编写的,一个用于左侧,一个用于右侧。
    • 左侧容器将 InboxScreen 解析为 Custom inbox view,构建一个,并将 rendering 传递给这个新视图。
    • 右侧容器对 MessageScreen 执行相同的操作,创建一个 Custom message view 来显示它。

迟早 EmailBrowserWorkflow 或其子 Workflow 的状态会发生变化。也许收到了新消息。也许因为用户现在想阅读其他内容,InboxScreen 上的事件处理函数被调用了。无论更新发生在 Workflow 层级结构的哪个位置,整个树都会被重新渲染:EmailBrowserWorkflow 将被要求提供一个新的 Rendering,它也会要求其子 Workflow 提供相同的 Rendering,以此类推。

是的,当任何东西发生变化时,所有东西都会渲染

新的 Workflow 开发者通常在听说任何状态在任何地方更新时整个树都会被重新渲染时会感到震惊。请记住,render() 实现应该具有幂等性,并且它们的工作严格是声明性的:render() 实际上意味着“我假设这些子 Workflow 正在运行,并且我订阅了这些工作流。请确保情况保持不变,或者在需要时启动一些新的工作流。”另一种思考方式是,它们声明了如何将内部 State 适配为外部 Rendering。这些调用应该很廉价,所有实际工作都发生在 render() 调用之外。

优化可能会阻止明显冗余的 rendering 调用发生,但在语义上应该假设当世界的任何部分发生变化时,整个世界都会被渲染。

一旦运行时的 Workflow 树完成重新渲染,新的 SplitScreen 将通过原生视图系统,如下所示

  • Workflow 根容器再次将新的 SplitScreen 传递给它的 Workflow 容器,因为这是它唯一知道的技巧。
    • 该容器认识到 SplitScreen 可以被它上次创建的 Custom split view 接受,因此无需做任何工作。
    • 现有的 Custom split view 接收新的 SplitScreen
  • 就像上次一样,Custom split viewInboxScreen 传递给它左侧的 Workflow 容器,并将 MessageScreen 传递给它右侧的容器。
    • 左侧的 Workflow 容器发现它已经在显示 Custom inbox view,于是将 InboxScreen rendering 传递过去。
    • 同样的事情也发生在 MessageScreen 和右侧 Workflow 容器之前构建的 Custom message view 上。

与视图代码一样,Custom inbox viewCustom message view 应该小心编写,以避免重复工作,比较它们已经显示的内容和现在被要求显示的内容。(一种简单的方法是将 Screen 类型的显示数据与其事件处理程序分开保存在一个对象中,例如 Equatable Swift struct 或 Kotlin data class。始终在一个 var 中保留最新的 Screen,并编写 UI 点击处理程序来引用它。)

如果任何 Screen Rendering 的类型发生变化,更新场景就会不同。假设我们的电子邮件应用能够在收件箱中同时托管电子邮件和语音邮件,并且上次更新中的 MessageScreenVoicemailScreen 替换了。在这种情况下,Custom message view 会拒绝新的 Rendering,创建它的右侧 Workflow 容器会销毁它。取而代之的会创建一个 Custom voicemail view,并且这个新视图会用 VoicemailScreen 中的信息来绘制自己。

那么这些容器是如何知道为哪些 Screen 类型创建哪些视图的呢?这些细节因语言和平台而异,并在下一页的“从 Screens 构建视图”部分进行了介绍。

ViewEnvironment

待定