Android App 用户体验
我们常见的 Android App 一般是由四大组件组成的,其中最常见的是 Activity 和 Service 等。一个 App 可能包含多个组件,而且移动设备资源有限,系统可能会随时终止某些 App ,鉴于这种情况,App 被销毁是不受开发者控制的,那么 App 的数据和状态就不应该在内存中进行存储,而且, 不同的组件之间不应该有依赖关系。
基于上述的一些客观情况,Android 为我们提供了一套开发的基本规则和从系统 API 层面的规避风险的支持。但作为开发者,也要注意避免这些情况的出现。
常见的架构原则
随着 Android 应用大小不断增加,App 的架构应该具有支持扩展、提升稳定性和方便测试等特性。 应用架构主要是为了划分各个部分之间的界限以及每个部分应该承担的职责。 官方为我们指定了一些设计原则让我们更好的去搭建和设计应用架构。
原则一:分离关注点
分离关注点(Separation of concerns,SoC),是将计算机程序分隔为不同部分的设计原则。每一部分会有自己需要关注的职责。这是最重要的原则。
一种常见的错误是在一个 Activity 或 Fragment 中编写所有的代码。这些基于界面的类应该仅处理 UI 和操作系统交互的逻辑
这些类应该尽可能的保持精简,这样可以避免许多与组件生命周期相关的问题,并且能够提升这些类的可测试性。
原则二:通过数据模型驱动界面
第二个重要的原则是,应该通过数据的变化来驱动 UI 的变化。数据模型就是 Model ,代表应用的数据,它们应独立于应用中的 UI 元素和其他组件,并且不受生命周期影响。但 Model 仍会在 App 销毁时,从内存中移除。
持久化数据是理想之选,原因如下:
- 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
- 当网络连接不稳定或不可用时,应用可以继续工作。
通过 Model 来驱动 UI ,另一个好处就是更方便进行测试。我们可以通过修改数据来测试 UI 的变化。
推荐的应用架构
基于上面的架构原则,App 至少应该分为两个层级:
- 界面层(UI):在屏幕上显示应用数据。
- 数据层(Model):包含应用的业务逻辑并公开应用数据。
可以增加一个中间的 Domain 层,来简化和重复使用界面层与数据层之间的交互。
UI 层
UI 层的作用是在屏幕上显示应用数据。无论是因为用户互动(例如按下按钮)还是外部输入(例如网络响应)导致数据发生变化时,UI 都应更新以反映相应的变化。
UI 层可以进一步拆分成两个部分:
- 在屏幕上呈现数据的 UI 元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
- 用于存储数据、向 UI 提供数据以及处理逻辑的状态容器(如 ViewModel )。
界面的状态
UI 层高可以拆分成 UI 元素和 UI 状态两部分。
UI 元素就是 UI 中具体的 View ;UI 状态是相对于应用而言的,可以理解为 UI 的数据逻辑抽象。当数据逻辑抽象发生变化时,就会导致呈现的 UI 的变化。
一个简单的类比是,通过 ViewModel 控制 UI 状态。
UI 的状态应该是定义为不可变的,这样能够使 UI 专注于发挥单一的作用。所以,不应该在界面中直接修改 UI 状态,除非 UI 是状态数据的唯一来源。
这个意思就是,相当于一个 Switch 控件,当你操作 UI 时,会使它的 一个 flag 在 true / false 之间进行切换。这就是一个 UI 是状态数据的唯一来源。
违反这个原则会导致同一条信息有多个可信来源,从而导致数据不一致等轻微的 bug。
单向数据流管理 UI 状态
UI 是 UI 状态的呈现, UI 状态是数据逻辑,在客户端中,一般数据都是动态变化的,不管是用户操作还是网络请求,都会导致 Model 层的数据更新,进而影响到 UI 状态的数据发生变化。
而单项数据流(一种架构模式),有助于强制实施这种健康的职责分离。
我们在上面提到过 ViewModel 组件是一个很好的状态容器。 通过 ViewModel 组件, Model 层和 UI 层进行互动的逻辑是:
状态向下流动、事件向上流动的这种模式称为单向数据流 (UDF)。这种模式对应用架构的影响如下:
- ViewModel 会存储并公开 UI 要使用的状态。UI 状态是经过 ViewModel 转换的应用数据。
- UI 元素会向 ViewModel 发送用户事件输入的消息。
- ViewModel 会处理用户操作并更新状态。
- 更新后的状态将反馈给 UI 元素以进行呈现。
- 系统会对导致 UI 状态更改的所有事件重复上述操作。
一个简单的例子是,一个文章阅读 App ,用户为这篇文章添加到收藏:
逻辑类型
上面的例子中,有一些逻辑需要进行定义或者说是为其划分层级:
- 业务逻辑:业务逻辑决定了如何处理状态的变化,通常位于中间层或 Model 层中,但绝不能位于 UI 层中。
- 界面行为逻辑/界面逻辑:会导致 UI 呈现内容的变化,这一层逻辑应该位于 UI 层中(尤其是涉及 Context 等界面常有的类型时),而非 ViewModel 中。
如果界面变得越来越复杂,并且您希望将界面逻辑委托给另一个类,以便有利于进行测试和关注点分离,您可以创建一个简单的类作为状态容器。
为什么要使用单项数据流?
单项数据流建立了上面这张图的模型,种分离可让界面只发挥其名称所表明的作用:通过观察 UI 状态变化来显示信息,并通过将这些变化传递给 ViewModel 来传递用户的输入。 换句话说,单项数据流有助于实现以下几点:
- 数据一致性:界面只有一个可信来源。
- 可测试性:状态来源是独立的,因此可独立于界面进行测试。
- 可维护性:状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。
而 UI 状态这一层级,可以以 ViewModel 作为容器,以 LiveData 作为状态的数据类传递工具(通过观察者模式,可以让 View 感知到状态数据的变化)。
数据层
Model 层应该包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。
数据层可以由多个数据管理的 Repositories 组成,每个 Repositories 可以包含零到多个数据源。
Repositories 类负责以下任务:
- 提供数据。
- 对数据进行集中管理。
- 解决多个数据源之间的冲突。
- 包含业务逻辑。
每个 Repositories 类应仅负责处理一个数据源,该数据源可以是文件、网络来源或本地数据库。
需要注意的是,Model 层的数据对象会保存在内存中,当 App 被系统销毁时,需要注意做数据的保存处理。
另一方面是数据对象应该是被强引用的,不会因为 GC 而造成数据的丢失。
Domain 层(可选层)
是位于界面与数据层之间的可选层。负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。
总结
尽管官方的推荐的这套架构,并没有明确指明是 MVVM ,但是从思想上还是和 MVVM 十分相似的。更准确的说,他更多的是 Presentation Model 模式的思路。 PM 的思想是将 View 抽象出一层 View 的抽象,然后通过 View 的抽象去与 Model 进行交互。 而在这个推荐的架构中,对 View 的抽象就是 UI State ,UI 元素则是指 View 。 通过 UI 状态去与 Model 进行交互也对应了 PM 中 , Presentation Model 的角色。