欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Google打造应用背后的架构设计步骤流程详解

最编程 2024-02-07 18:53:14
...

我正在参加「掘金·启航计划」

对app来说,即便将所有代码都怼一起也能运行,但代码难以维护,随着时间的累积,代码逐渐堆积成屎山。为了防止架构腐坏,先后有MVC、MVP、MVVM等各种架构都被提出,但他们或多或少都有一些不足之处,随着android系统的发展及其开发生态越来越成熟,google推荐的应用架构也渐趋完善,正好最近学习了官方架构,作为学习成果,今天就来对其做个总结。

架构原则

设计架构就是划分不同模块的职责以及他们的边界,并应遵循如下原则:

  • 分离关注点
  • 通过数据模型驱动界面
  • 单一数据源
  • 单向数据流

应用架构

根据分离关注点原则,app可以划分为UI层和Data层,当存在多处公用的复杂业务逻辑时,可以在其中添加domain层进行处理,整体架构图如下:

在典型的应用架构中,界面层会从数据层或可选网域层(位于界面层和数据层之间)获取应用数据。

UI层

UI层是app的核心部分,大部分功能都和UI相关,所以UI层的设计十分重要。UI层由UI元素、状态以及处理状态的逻辑组成,因此可以将其分为如下两部分:

  • UI元素(UI Elements):对于app屏幕中的各种UI组件,主要负责绘制工作
  • 状态容器(State holders):提供UI状态,并进行相应的逻辑处理。

在典型架构中,界面层的界面元素依赖于状态容器,而状态容器又依赖于来自数据层或可选网域层的类。

UI状态及事件

通常情况下UI元素由View或者Component组件组成,他们根据UI状态进行更新,因此要根据应用需求将UI状态定义好 。UI状态定义完成后,需要思考用户操作产生的事件,将其归类,选择适当的位置处理这些事件。

定义UI状态

前面说到UI元素根据状态更新界面,即界面由UI元素UI状态组成,不同的UI状态代表着不同时刻UI元素显示的效果,如下图:

界面是将屏幕上的界面元素与界面状态绑定在一起的结果。

考虑一款电影票售票应用,在选座购票时,选中和没选中就是座位的两个不同状态,其状态定义如下:

data class SeatUiState(
    val selected: Boolean = false
)

通常来说,在定义UI状态时要保证其不可变性, 这样能保证UI元素功能的纯粹性:仅根据状态更新界面,这样有利于对界面层进行单元测试,仅需要对状态容器行为及其发出的状态进行校验即可,从而省去许多复杂低效的UI测试。这意味着不能再界面中修改UI状态,这导致同一个状态有多重来源(状态容器和界面元素),不仅违反了单一数据源原则,也容易产生数据不一致的问题。

事件决策树

UI事件就是用户操作引用所产生的各种事件,可以分为与业务逻辑相关事件和纯界面逻辑事件:

  • 业务逻辑通常指处理应用数据的逻辑,如付款或存储用户设置,通常由domain和data层负责处理此逻辑。在android应用中,ViewModel 是处理业务逻辑的特色解决方案。
  • UI行为逻辑(即UI逻辑)是指处理UI绘制及显示的逻辑,如控制Button的显示或隐藏。

我们可以使用决策树的方式对UI事件进行分类:

如果事件源自 ViewModel,则更新界面状态。如果事件源自界面并需要业务逻辑,则将业务逻辑委托给 ViewModel。如果事件源自界面并需要界面行为逻辑,则直接在界面中修改界面元素状态。

总体规则就是:如果用户事件与修改UI元素的状态(如可展开项的状态)相关,界面便可以直接处理这些事件。如果事件需要执行业务逻辑(如刷新屏幕上的数据),则应用由 ViewModel 处理此事件。

状态容器

状态容器负责实现具体的业务逻辑,实现状态容器前,需要了解app的状态流向、容器分类以及他们各自的职责。

状态管理

一般使用单项数据流 (UDF)管理状态,这有助于实现不同层之间的职责分离。由于界面和 ViewModel 类的互动在基本可以理解为事件输入及其状态输出,因此这种关系可以用下图表示:

应用数据从数据层流向 ViewModel。界面状态从 ViewModel 流向界面元素,事件从界面元素流回 ViewModel。

所谓单项数据流就是状态向下流动、事件向上流动,数据流向如下:

  • ViewModel 会存储并公开UI要使用的状态,UI状态是经过 ViewModel 转换的应用数据。
  • 界面会向 ViewModel 发送用户事件通知。
  • ViewModel 会处理用户操作并更新状态。
  • 更新后的状态将反馈给界面进行绘制。
  • 系统会对导致状态更改的所有事件重复上述操作。

要注意ViewModel中不能包含UI逻辑(如:获通过Resources获取文本、点击时跳转、打开弹窗等逻辑)。如果界面变的过于复杂,依据关注点分离原则:需要创建一个简单类作为状态容器,对内部逻辑进行二次划分。状态容器尽量不要依赖Android SDK,而在Activity、Fragment、View中创建的类可以依赖。

UI状态生成流水线

状态容器核心工作就是生成UI状态,发送给界面进行处理,因此存在一条状态流水线,但介绍UI状态生成流水线之前需要先总结其中的一些术语。

UI状态

UI状态是描述界面的属性。界面状态有两种类型:

  • 屏幕UI状态是需要在屏幕上显示的内容。
  • UI元素状态是指界面元素的固有属性,这些属性会影响界面元素的显示方式。界面元素可能处于显示或隐藏状态,并且可能具有特定的字体、字体大小或字体颜色。在 Android View 中,View 会自行管理此状态(因为它本身是有状态的),并公开用于修改或查询其状态的方法。例如,TextView 类的 getset 方法用于显示该类的文本。

逻辑

应用中的逻辑可以是业务逻辑或界面逻辑:

  • 业务逻辑用于处理domain或data层的数据。
  • UI逻辑则代表UI组件的控制逻辑。

其与android生命周期的关联如下:

不依赖于界面生命周期 依赖于界面生命周期
业务逻辑 界面逻辑
UI状态

状态流水线通常按照如下顺序生成:

  • 直接在UI逻辑中生成和管理的UI状态,当数据不是来自data层,且无需将数据传递到外层时就可以才用这种方式管理UI状态,例如,一个简单且可重复使用的基本计数器。
  • UI逻辑 → UI。例如,控制允许用户跳转到列表顶部的按钮的显示或隐藏。
  • 业务逻辑 → UI。比如获取用户头像并展示。
  • 业务逻辑 → UI逻辑 → 界面。比如:给定UI状态之后,需要滑动到指定位置显示。

总体流程如下:

从数据生成层流向界面的数据流

通常情况下,如果同时用到业务逻辑和UI逻辑,必须先应用业务逻辑,再应用UI逻辑,如果先应用UI逻辑,再应用业务逻辑,则意味着业务逻辑依赖于界面逻辑,这样破坏了UI的分层关系,增加代码耦合性,难以测试。

状态容器分类

由于app中的逻辑分为业务逻辑和UI逻辑两种,因此设计两种状态容器分别处理业务逻辑和UI逻辑:

  • 业务逻辑状态容器。
  • UI逻辑状态容器。

业务逻辑状态容器有如下特性:

属性 详细信息
生成界面状态 业务逻辑状态容器负责为其界面提供界面状态。此界面状态通常是处理用户事件以及从网域层和数据层读取数据的结果。
在 activity 重新创建后保留下来 业务逻辑状态容器会在 Activity 重新创建后保留其状态和状态处理流水线,从而帮助提供无缝的用户体验。如果会重新创建(通常是在进程终止后)状态容器,但无法保留其状态,则状态容器必须能够轻松地重新创建最近一个状态,以确保一致的用户体验。
具有长期存在的状态 业务逻辑状态容器通常用于管理导航目的地的状态。因此,它们往往会在导航发生变化时保留其状态,直到从导航图中移除它们为止。
对界面来说独一无二,且不可重复使用 业务逻辑状态容器通常会针对某个应用函数(如 TaskEditViewModelTaskListViewModel)生成状态,因此仅适用于该应用函数。同一状态容器可以支持在不同外形规格的设备上使用这些应用函数。例如,应用的手机版本、电视版本和平板电脑版本都可以重复使用同一个业务逻辑状态容器。

业务逻辑状态容器通常用 ViewModel 实现,不过可以根据app的实际情况使用普通类。

UI逻辑状态容器对UI自身数据的操作逻辑,他可能依赖UI元素状态或UI数据源(如权限API或Resources),通常具有如下属性:

  • 生成UI状态并管理界面元素状态
  • Activity 重新创建后不再有效:托管在UI逻辑中的状态容器通常依赖于UI本身的数据源,在配置发生变化后保留对应数据容易导致内存泄漏。
  • 引用了UI范围的数据源:生命周期 API 和资源(Resources)等数据源可以安全地引用和读取,因为UI逻辑状态容器与UI具有相同的生命周期。
  • 可在多个不同的界面中重复使用:同一UI逻辑状态容器的不同实例可以在应用的不同部分中重复使用。

UI逻辑状态容器通常使用普通类实现,当UI逻辑足够复杂,可以移出界面时,会使用普通类状态容器。否则,UI逻辑可以在界面中以内嵌方式实现。

线程处理和并发

ViewModel中执行异步操作时应该使用Kotlin协程,如果是domain或者data层需执行耗时操作,应由他们自行负责线程切换工作。

UI层架构通过UI状态将业务逻辑与UI实现项分离,这样设计提升了业务逻辑的可测试性,并且降低在手机、平板、电视这些不同设备之间的适配难度。

Domain层

Domain层是一个可选层,负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑,如果应用比较简单,可以无需设计这一层。

如果添加了此层,则该可选网域层会向界面层提供依赖项,而它自身依赖于数据层。

domain层具有以下优点:

  • 避免代码重复。
  • 提升代码可读性:由于domain层将较为复杂的逻辑封装好了,使得引用domain的类更为简洁。
  • 改善应用的可测试性。
  • 明确职责,避免出现大型类。

domain层的类通常称为UseCase,即用例。用例通常用于组合data层逻辑,从而实现更为复杂的逻辑,比如将新闻列表和对应的作者数据组合:

GetLatestNewsWithAuthorsUseCase 依赖于数据层中的仓库类,但它同时还依赖于同样位于网域层的另一个用例类 FormatDataUseCase。

用例没有自己的生命周期,所以可以在UI层的类、Service以及Application中调用用例。

用例类必须具备主线程安全性,如果用例类存在长时间阻塞的操作,由他们自行将任务切到子线程中处理。

Data层

Data层包含应用数据和业务逻辑,它由多个仓库组成,其中每个仓库都可以包含零到多个数据源。应该为每种不同类型的数据分别创建一个存储库类。

在典型架构中,数据层的仓库会向应用的其余部分提供数据,而这些仓库则依赖于数据源。

存储库类负责以下任务:

  • 向应用的其余部分公开数据。
  • 集中处理数据变化。
  • 解决多个数据源之间的冲突。
  • 对应用其余部分的数据源进行抽象化处理。
  • 包含业务逻辑。

每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。数据源类是应用与数据操作系统之间的桥梁。

层次结构中的其他层绝不能直接访问数据源;数据层的入口点始终是存储库类。状态容器类(请参阅界面层指南)或用例类(请参阅网域层指南)绝不能将数据源作为直接依赖项。如果使用仓库类作为入口点,架构的不同层便可以独立扩缩。

该层公开的数据应该是不可变的,这样就可以避免数据被其他类篡改,从而避免数值不一致的风险。不可变数据也可以由多个线程安全地处理。

按照依赖项注入的思想,存储库应在其构造函数中将数据源作为依赖项。

多层存储库

在某些涉及更复杂业务要求的情况下,存储库可能需要依赖于其他存储库。这可能是因为所涉及的数据是来自多个数据源的数据聚合,或者是因为相应职责需要封装在其他存储库类中,如下:

在示例中,UserRepository 依赖于另外两个存储库类,即依赖于其他登录数据源的 LoginRepository 和依赖于其他注册数据源的 RegistrationRepository。

线程处理

调用数据源和存储库应该是主线程安全的,在执行长时间运行的阻塞操作时,这些类负责将其逻辑的执行移至适当的线程。

数据

通常情况下,app的显示数据和接口数据都存在差别,这种情形下需要分离模型类,所谓分离模型类就是根据app的显示需求定义新的模型类,删减掉接口中的部分数据。分离模型类可以带来以下好处:

  • 将数据减少到只包含需要的内容,从而节省应用内存。
  • 根据应用所使用的数据类型来调整外部数据类型 - 例如,应用可以使用不同的数据类型来表示日期。
  • 更好地分离关注点 - 例如,如果预先定义了模型类,大型团队的成员便可以在功能的网络层和界面层单独开展工作。

引用

应用架构指南