移动客户端的主要职责是根据从后台接收到的数据渲染组件, 这些数据通常决定了应该显示什么. 这意味着客户端开发人员必须专注于两项关键任务:了解如何处理后端提供的数据, 以及如何将数据呈现为特定平台的组件.
有几种众所周知的方法可以减轻移动客户端的负担, 同时提高提供应用功能或内容的灵活性. 其中一个例子就是使用基于网络的内容, 这样就可以在不等待 Google Play 审核流程的情况下进行更新. 但是, 如果你的服务需要为用户提供高性能, 那么网络应用可能并不适合所有情况.
对于那些同时追求功能交付的灵活性和本地应用的性能优势的用户, 可以探索另一种方法: 在后台设计布局结构和后台 Action Handler , 而在客户端被动地呈现和处理行为. 这种方法通常被称为服务器驱动的UI(SDUI)或服务器端渲染(SSR).
在本文中, 你将了解服务器驱动UI的概念, 利弊, 如何使用 Jetpack Compose 实现它, 以及如何使用 Firebase Realtime Database 配置你的后台.
通常情况下, 移动客户端负责呈现后端提供的数据, 布局主要由客户端决定. 这意味着移动开发人员必须根据他们的 UI/UX 指南来设计每个组件的布局, 同时还要管理每个组件的呈现细节, 如下图所示:
另一方面, 服务器驱动的UI(SDUI)是一种架构模式, 旨在最大限度地减少客户端逻辑, 并确保跨多个平台(Web, iOS, Android 等)的一致性. API 不返回原始域数据, 而是提供布局信息, 允许动态使用UI, 减少对特定平台代码的需求, 如下图所示:
因此, 客户端变得更加被动, 只需从后台接收带有布局信息的数据即可. 这种转变使开发人员能够专注于如何呈现数据, 而不是呈现什么, 从而为提供内容提供了更大的灵活性. 它还能让开发人员专注于组件开发, 而不是管理复杂的布局系统.
Airbnb 和 Netflix 等许多大公司的主要产品都采用了这种方法. 还有一些著名的解决方案, 如Epoxy 和Litho, 它们允许开发人员在传统的 Android View 系统之上, 以声明的方式实现复杂的屏幕.
在大多数情况下, 原生应用的发布过程都很缓慢, 很难让用户随时更新最新版本. 有时, 你甚至需要强制用户更新应用以修复关键问题, 这可能会对UE造成负面影响. 传统的本地应用通常面临以下挑战:
- 更新周期短: 耗时的发布过程会导致关键更新的交付延迟.
- 用户采用率低: 用户需要手动下载和安装更新, 导致对新功能和错误修复的采用速度减慢.
- 功能实验较少: 由于更新周期较慢, 对团队希望测试的特定功能进行实验和迭代变得具有挑战性.
- 反馈循环慢: 由于更新周期和采用率较慢, 收集用户反馈和实施快速更改变得更加困难.
在许多情况下, 你可以使用 Firebase Remote Config通过启用或禁用功能来进行试验. 但是, 当需要更大的灵活性时, 例如重新构建整个屏幕布局时, 这种方法就会显得力不从心. 那么, 为什么要选择服务器驱动的UI呢?以下是你可以利用的一些关键优势:
- 更快的功能实验: 你可以轻松修改和部署新布局, 而无需用户更新应用. 这将大大加快反馈循环和迭代速度.
- 跨应用版本的一致UI: 通过建立稳固的组件设计系统, 只要核心规范保持稳定, 用户就能在多个应用版本中体验到一致的UI和行为.
- 原生性能: 在受益于服务器驱动UI的灵活性的同时, 你仍然可以获得渲染组件的本地性能.
- 减轻移动开发人员的负担: 布局设计和应用规范通常由PM和设计师而不是移动开发人员来定义. 有了服务器驱动的UI, 布局设计的责任就可以转移给PM, 让开发人员专注于根据后台数据构建和渲染各个组件.
一如既往, 没有放之四海而皆准的解决方案. 虽然服务器驱动的UI具有多种优势, 但在集成到产品中时也会面临挑战.
- 延迟和性能: 这是最大的挑战之一. 由于客户端需要从后台获取布局信息, 因此与纯本地应用相比, 渲染和显示组件可能需要更长的时间. 此外, 渲染还依赖于互联网连接, 这会影响性能.
- 复杂性和成本: 你的整个团队必须建立明确的布局系统协议, 例如如何在客户端呈现布局数据, 组件版本控制和角色分配. 如果没有一个设计良好的系统, 让PM能够轻松生成布局信息并上传到后台, 后台团队可能会承担创建复杂布局的负担, 或者客户团队需要经常与后台团队协作, 从而可能增加复杂性和成本.
- 适当的回退处理: 数据并不总是完美的, 错误也可能发生. 如果PM设计了一个有缺陷的布局系统, 或者后台数据被破坏, 客户必须通过适当的回退来优雅地处理这些错误, 以确保流畅的UE.
归根结底, 服务器驱动的UI会增加开发的复杂性和团队成本, 因为它涉及到整个团队的协调. 因此, 在实施之前必须仔细评估这种方法是否合适. 在许多情况下, 你可以逐步引入服务器驱动的UI, 首先从高度灵活的屏幕开始, 例如应用的主屏幕, 在那里动态更新可能会带来最大的价值.
现在, 让我们使用开源项目server-driven-compose深入了解实施细节, 该项目使用 Firebase 演示了 Jetpack Compose 中的服务器驱动UI方法.
server-driven-compose 项目选择了Firebase Realtime Database 作为实时可视化UI变化的后端. 不过, 在实际应用场景中, 一般建议通过自己的后端系统来实现服务器驱动的UI, 因为UiComponent通常不需要实时更新. 另外, 如果 Firestore 更符合你的要求, 你也可以考虑使用它.
你可以按照以下步骤轻松配置 Firebase Realtime Database:
- Firebase设置: 按照 Firebase 设置指南 下载
google-services.json
文件, 并将其放到本项目的app
目录中. - Realtime Database设置: 在 Firebase 面板上设置 Firebase Realtime Database. 完成后, 即可导入 JSON 文件.
- 下载 JSON 文件: 下载时间轴UI的演示 JSON 文件(作为 Gist 提供)到本地计算机.
- 配置数据库 URL: 在项目根目录下创建名为
secrets.properties
的文件, 并将Realtime Database URL 复制粘贴到该文件中, 如下图所示.
REALTIME_DATABASE_URL=https://server-driven-compose-default-rtdb.asia-southeast1.firebasedatabase.app/
Firebase Realtime Database KTX
官方Firebase Realtime Database的一个主要局限是, 它严重依赖于 Java 和回调监听器, 因此与 Kotlin Coroutine 和 Jetpack Compose 的兼容性较差. 此外, 由于它以非JSON 格式返回快照值, 因此处理数据对象和实现自定义序列化变得复杂. 例如, 下面的示例说明了处理此类快照值所面临的挑战:
由于数据不是 JSON 格式, 直接序列化到目标对象并不可行. 为了解决这个问题, firebase-android-ktx 库提供了一种对 Kotlin 更友好的解决方案. 它允许你以 Flow 的形式观察Realtime Database中的变化, 并提供完全可定制的序列化选项. 这简化了数据流的处理, 并使数据格式适应你应用的要求, 确保与 Kotlin Coroutine 和 Jetpack Compose 的顺利集成.
要开始使用该库, 请在应用的 module.gradle.ktx
文件中添加以下依赖关系:
你可以使用 flow()
方法和自定义序列化方法, 将快照数据以 Flow
的形式持续观测到 DatabaseReference
实例的变化, 如下例所示:
完成这些步骤后, 你就可以在本地构建服务器驱动的 compose 项目了. 然后, 你就可以在Realtime Database仪表板中操作值, 如下图所示, 更改将实时反映在应用中.
现在, 让我们来设计呈现界面. 通常情况下, 客户端接收 JSON 格式的数据, 其中只包含特定领域的信息. 客户端负责处理这些原始数据, 并将其转换为适合绑定到客户端预定义布局的格式, 如下图的 JSON 响应所示:
这种方法可称为领域驱动界面
, 其中的数据仅包含核心领域信息, 而更高层次的细节(如文本大小或颜色)则由客户端的表现逻辑来处理.
在服务器驱动的UI中, 方法略有不同. 它包括可直接应用于客户端的动态信息, 如文字大小, 颜色, 字体大小, 图像尺寸和其他可改变每个组件外观的设计属性. 这可以称为布局驱动界面.
在这种模式下, 服务器同时提供组件属性和域数据, 允许动态呈现UI. 下面是一个例子, 组件属性与域数据一起以 JSON 格式提供:
这就是关键所在, 尽管最初的印象可能会让人感到困惑和陌生, 因为这与传统方法不同. 在服务器驱动的UI中, 服务器提供组件属性和域数据的组合, 为客户端提供了极大的灵活性. 但缺点是, 响应可能会变得相当复杂, 与以前的方法相比, 后台工作量可能会大幅增加.
尽管有这样的挑战, 如果你的团队经常需要为特定屏幕或功能引入灵活性, 或者你经常尝试新的UI/UE风格, 那么这种方法可能非常适合你的需求.
理想的情况是开发一个系统, 让设计和产品团队可以创建布局驱动界面的初始版本, 从而减少对后台团队的依赖. 由于大多数UI/UE决策通常都是由设计团队做出的, 因此这种责任转移将简化开发流程.
现在, 让我们继续设计 Jetpack Compose 中的组件. 在深入实施之前, 考虑设计方法很重要. 你需要创建具有高度灵活性并能够使用布局驱动界面的组件. 这些组件应该是被动的, 这意味着它们不会决定布局, 而是根据 API 响应动态调整, 从而根据需要灵活地进行变化.
在基于 XML 的布局时代, 你需要 Epoxy 或 Litho 这样的解决方案来使用传统的 View 系统动态构建布局. 然而, 由于 Jetpack Compose 本身就是基于声明式UI方法的, 因此这一过程就变得简单多了. 你可以轻松构建自己的设计系统, 使动态布局更加灵活, 易于管理.
如果你看看 Server Driven Compose 项目中时间线屏幕的代码, 就会发现它是多么简单. 代码只需使用提供的布局信息和域数据, 而无需为每个组件或屏幕单独定义布局细节. 这种简单性在下面的代码片段中显而易见:
你可能对 UiComponent
是什么以及 UiComponent.Consume
函数的内部工作原理很好奇. 让我们深入研究一下服务器驱动 Compose 项目的设计系统, 以便更详细地了解这些组件.
UiComponent
代表了每个UI元素的最高级别抽象, 这意味着每个可通过服务器驱动UI方法呈现的组件都应从UiComponent
扩展而来. 这样, 你就可以通过 UiComponent
接口, 更灵活地处理各种类型的组件. 实现该接口的方式可根据项目的具体要求和场景而有所不同.
现在, 你需要使用后台提供的布局信息来定义你要渲染的每个 UI 组件. 这意味着封装布局数据并将其应用到组件的各个属性中.
例如, 你可以创建一个名为 TextUi
的数据类, 它封装了 Text
Composable元素的所有属性. 这样, 你就可以将这些属性灵活地应用到 Text
Composable文件中, 如下面的示例代码所示:
让我们来看另一个渲染图像的示例. 与 TextUi
数据类类似, 你可以定义一个名为 ImageUi
的数据类, 它封装了灵活呈现图像所需的所有属性, 如下代码所示:
如下代码所示, 这些属性可用于构建来自后台的响应:
现在, 我们已经从后台服务器获取了数据, 是时候将布局信息渲染为函数并显示在屏幕上了. 第一步是实现 UI 组件的基本版本, 确保它能反映后台提供的属性.
如果你收到了用于呈现文本的属性, 你可以创建一个函数来消耗TextUi
, 如下代码所示:
对于呈现图像, 你也可以采用同样的方法, 消耗 ImageUi
数据类.
你已经学会了如何绑定 Text
和 Image
等简单的 UI 组件. 现在, 让我们深入学习如何处理列表数据并将其绑定到函数中. 由于列表由多个项目组成, 因此结构可能会更复杂一些, 但其核心概念仍与其他组件类似. 首先, 让我们来看看后端服务器将提供的布局信息, 以便进行呈现:
此响应的父节点(list
)定义了布局类型(网格, 列或行), 可灵活设置的项目大小以及项目本身--在本例中为图片. 根据这一结构, 你可以创建一个 ListUi
模型来处理客户端的布局响应:
最后, 你需要在一个函数中消费 ListUi
信息. 你可以实现一个 ConsumeList
函数, 根据提供的布局类型渲染不同的列表样式, 如网格, 列或行. 在下面的示例中, 只实现了网格布局类型, 以进行演示.
通过与UiComponent
接口, 你现在应该已经很好地掌握了将布局渲染数据与函数相结合的概念. 现在, 是时候迈出下一步, 使用抽象方法将布局数据映射到函数中了.
假设屏幕分为三个部分: 顶部, 中间和底部. 你可以为这些部分配置如图所示的布局响应数据, 并在下面的示例中以流程的形式进行观察:
现在, 你应该将请求作为 ViewModel 中的一个状态来管理, 如下例所示:
如果你看一下上面的代码, 就会发现所有在 TimelineUi
中的组件都是通过 buildUiComponentList 方法放入一个 UiComponent
列表中的. 这种方法很有必要, 因为它可以将不同类型的组件统一到同一个 UiComponent
接口下, 从而使它们得到一致的处理和使用.
完成上述工作后, 就可以将每个UiComponent
映射到一个函数, 并使用布局数据来渲染 UI 组件了. 由于你已经将所有组件统一到了 UiComponent
接口下, 这可以通过实现一个名为 Consume
的扩展函数轻松实现, 如下例所示:
一个重要的考虑因素是如何处理回退, 例如当出现问题(如接收到未定义的组件类型)时显示回退视图. 在这个示例中, 回退情况是通过一个 ConsumeDefaultUi
函数来处理的, 以确保未定义的类型得到优雅的管理.
最后, 你可以使用 Column
或任何其他最适合你项目的布局容器来构建整体布局. 然后, 你就可以利用已实现的 Consume
扩展来呈现每个统一的 UiComponent
了, 如下面的代码所示:
结果, 你将看到以下输出:
你可能还会遇到一些挑战, 其中之一就是处理操作, 例如项目点击监听器和导航到其他屏幕. 如前几节所述, 在服务器驱动的UI方法中, 包括操作在内的所有组件行为都应由后台响应而非客户端定义.
这样, 客户端就可以专注于如何处理操作, 而不是处理什么操作. 在这种方法中, 动作处理逻辑作为组件界面的一部分通过后台交付, 从而确保了UI的一致性和灵活性.
例如, 如果需要在用户点击横幅图片时导航到详细信息屏幕, 可以在后台响应中定义 Action Handler 接口, 如下图所示:
然后, 应根据服务要求和目标定义 Action Handler 接口. 例如, 你可以指定每种Handler类型, 相应的操作以及导航, 深度链接或其他相关行为等细节.
接下来, 你可以将Handler参数添加到你的UI模型中, 如下例所示. 该参数可以为空, 因为并非所有组件都需要 Action Handler . 这样可以确保灵活性, 使没有动作的组件也能按预期运行.
现在, 你需要为每个组件使用Handler. 在本示例中, Handler 是通过实现名为 consumeHandler
的自定义 Modifier
扩展来处理的, 这简化了处理过程.
最后, 你可以通过将 consumeHandler
结合到组件的函数中来应用 Action Handler , 如下例所示:
在项目中引入服务器驱动的UI时, 你需要解决的一个主要问题是组件版本控制. 随着软件库的发展和 UI/UX 设计的变化, 预定义组件的结构和行为可能需要进行基本调整或添加新内容. 在这种情况下, 必须对组件实施适当的版本控制, 以有效管理这些变更.
首先, 你应该在响应的根目录或任何需要指定特定组件版本的地方包含版本信息, 如下面的 JSON 文件示例所示:
接下来, 你可以更新 TimelineUi
模型以包含版本信息, 如下代码示例所示:
此外, 你还可以使用枚举类定义不同的版本, 如下所示:
通过这种设置, 你可以根据提供的版本信息实现不同的组件版本. 例如, 你可以修改 ConsumeTextUi
函数, 如下所示:
为确保从响应中正确传递版本信息, 请更新 Consume
函数, 如下例所示:
最后, 每个组件都可以使用相应的版本信息进行渲染, 如下图时间线屏幕示例所示:
如果运行 Server Driven Compose项目, 你会发现每个组件都会根据响应中提供的指定版本以不同方式呈现:
在本文中, 你已经探索了服务器驱动UI的关键概念, 包括如何在 Jetpack Compose 中实现布局界面和设计系统. 此外, 你还了解了处理回退, Action Handler 和组件版本控制等高级主题.
Jetpack Compose 支持声明式的UI开发方法, 与传统的视图系统相比, 它更容易实现服务器驱动的UI方法. 不过, 虽然服务器驱动的UI方法提供了极大的灵活性(类似于网络应用), 并能保持比网络应用更强的性能, 但它也需要大量的初始投资, 以便在整个团队中建立必要的设计系统. 因此, 在决定在项目中采用这种方法之前, 仔细评估其优势和挑战至关重要.
今天的内容就分享到这里啦!
一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!