【现代 Android APP 架构】01. APP 架构综述

2,140 阅读13分钟

他老是生活在对未来的憧憬里,却接二连三地坐失眼前的良机。——毛姆《人生的枷锁》

我为什么写这个专栏

首先,我认为,人生的主旋律是主动去迎接变化和成长,而非被动地,生活给我们什么,我们就接受什么。不论是学业、生活还是职业发展,到达一定阶段后,必然要向下一个阶段迈进,扩大自己的人生边界。

我不想做一个固守在自己的舒适区、成长停滞的人,就是这么简单。人生一世,总要体验更多的事情、看更精彩的风景、与更多有趣的人交往。

而在互联网软件研发行业,作为程序员,其一生的成长路径可以概括为:

  1. 刚入门,啥都不会的新手
  2. 熟练工,能够独立完成一个明确需求的研发
  3. 高绩效的个人,为团队做出一定贡献,在团队的年终汇报里作为重要任务执行人被表扬
  4. 小团队的领导者,领导处于新手、熟练工阶段的人员承接更大更模糊需求的落地
  5. 更大范围的管理者,对内进行Hire and Fire,对外要对部门乃至公司的年度营收负责
  6. 技术总裁,创业公司技术负责人

在这个成长路径中,要完成一系列很重要的转变,即 职责范围(Scope)的扩大化。从只负责承接一个明确的需求,写代码实现其功能,转变为——向上承接多个模糊需求,拉通相关方进行细化和评估,向下拆分任务,汇总进度并识别风险给出预案。

这是一个从 高绩效个人小团队领导者 转变的过程。在这个过程里,有三项能力必须达标:

  • 横向拉通对齐的能力: 理解项目通用流程,其中有哪些关键节点,有技巧地说服相关方采用积极态度配合,从而拿到不错的结果;不疾不徐、结构化、有条理的表达沟通,既能让听的人快速 get 到意图,又不至于使其产生抗拒心理
  • 软件架构的能力: 对项目软件架构有深刻的理解,综合考虑应用 性能、成本、伸缩性、可维护性、安全合规、出海适配 等因素,进行技术选型,达到一个 均衡点,这个均衡点就是实现业务需求的最佳架构方案
  • 技术难点攻关能力: 对开发语言特性理解深入,形成一套结构化的问题分析框架,别人解决不了的bug你能解决;具备快速学习上手一门新技术、新语言并应用的能力。

至少要做到以上这些,自己才能成为一个别人眼里 职业化 的人,才能更好地为进入下一阶段打好基础。

其中 架构能力 是技术与产品的结合点,是技术价值的根本所在,我们不能脱离具体的需求场景来谈论技术,这是闭门造车,空有一身屠龙之术却无用武之地。技术永远是为业务和需求服务的,一把钥匙开一把锁,我们要做的就是成为那个会打造各种各样钥匙的人,做一个会开锁、擅长开锁、高效率结构化开锁的人。

因此,架构能力 会是短期(1~3个月)内自己的学习重点。更具体点,是 Android APP 的架构分析和设计能力。

设计 APP 架构时应当遵守的原则

应用架构定义了应用的各个部分之间的界限以及每个部分应承担的职责。为了满足上述需求,我们应该按照某些特定原则设计应用架构,这些原则是一切的出发点,是软件架构的 第一性原理

关注点分离

说它是软件架构最重要的原则都不为过,在2012年前后的 Android 开发早期,我们还是把所有代码放在一个 Activity 当中处理。随着业务迭代得更加复杂,开发团队规模也随之增大,这种写法早已无法满足并行开发的需要。

在当代 Android 开发中,Activity/Fragment 只是作为业务代码与操作系统之间的 UI 呈现层,它是可以被自由替换的,例如,可以将前端页面改为 原生、H5、Flutter 等多种平台实现,而无需变更业务层代码。操作系统可能会根据用户互动、或因内存不足等系统条件随时销毁这些 UI 对象。为了提供令人满意的用户体验和更易于管理的应用维护体验,最好尽量减少对它们的依赖。

数据驱动UI

数据模型(Data Model)代表应用的数据。它们独立于应用中的界面元素和其他组件。这意味着它们与界面和应用组件的生命周期没有关联,但仍会在操作系统决定从内存中移除应用的进程时被销毁。

持久化 是必须要考虑的点,原因如下:

  • 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
  • 当网络连接不稳定或不可用时,应用会继续工作。

此外,以数据层为基础的应用架构,会更便于进行测试。

单一可信数据源

单一可信数据源(Single Source Of TruthSSOT)是数据的所有者,所有数据的根本来源。只有 SSOT 可以修改和转化数据,在其上层的调用者只能被动接收该不可变数据。

SSOT 对外以接口的方式,提供修改数据的能力。这种设计模式的优点如下:

  • 将对特定类型数据的所有更改集中到一处。
  • 保护数据,防止其他类型篡改此数据。
  • 更易于跟踪对数据的更改。因此,也就更容易发现 bug。

在离线优先应用中,应用数据的单一数据源通常是数据库。在其他某些情况下,单一数据源可以是 ViewModel 甚至是界面。

单向数据流

单一数据源原则(SSOT)常常与单向数据流 (Unidirectional Data FlowUDF) 模式一起使用。在 UDF 中,状态仅朝一个方向流动(数据流)。修改数据的事件朝相反方向流动(控制流)。

在 Android 中,状态或数据通常从分区层次结构中较底层的分区类型流向较上层的分区类型。事件通常在分区层次结构中较上层的分区类型触发,直到其到达 SSOT 的相应数据类型。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向 SSOT,在 SSOT 中应用数据被修改并以不可变类型公开。

此模式可以更好地保证数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。

Clean Architecture 是现代 APP 架构的终极形态么

Clean Architecture 概述

基于上一部分提到的常见架构原则,每个应用应至少有两个层:

  • 界面层: 在屏幕上显示应用数据。
  • 数据层: 包含应用的业务逻辑并公开应用数据。

可以额外添加一个名为 领域层 的架构层,以简化和复用界面层与数据层之间的交互。

此图片中的箭头表示 依赖关系

现代应用架构鼓励采用以下方法及其他一些方法:

  • 响应式分层架构。
  • 应用的所有层中的单向数据流 (UDF)。
  • 包含状态容器的界面层,用于管理界面的复杂性。
  • 协程和数据流。
  • 依赖项注入最佳实践。

界面层

界面层(或表现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

界面层由以下两部分组成:

  • 在屏幕上呈现数据的界面元素。可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。

数据层

应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。

数据层由多个仓库组成,其中每个仓库都可以包含零到多个数据源。应该为应用中处理的每种不同类型的数据分别创建一个数据仓库(Repository)类。例如,可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。

数据仓库类负责以下任务:

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

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

领域层

领域层是位于界面与数据层之间的可选层。

领域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如 处理复杂逻辑 或支持 可重用性

此层中的类通常称为 “用例”“交互方”。每个用例都应仅负责单个功能。例如,如果多个 ViewModel 依赖 时区 在屏幕上显示适当的消息,则您的应用可能具有 GetTimeZoneUseCase 类。

回答最初的问题 —— Clean Architecture 是现代 APP 架构的终极形态么?

我认为的答案—— 是的。在 关注点分离、伸缩性、易扩展性、可测试性、与 Android SDK 的良好隔离 方面,Clean Architecture 都做到了目前的极致。在 Android 原生组件不发生剧烈变化的前提下,Clean Architecture 仍然会继续成为架构设计的主流。

通过注入自动进行构建

常见最佳实践

编程是一门艺术,有其自身的设计感和美学,就像阅读一篇内容精巧的小说,草蛇灰线,伏笔千里,读完让人扼腕惊叹。

对于一个问题,解决它的方法有多种,架构师要做的就是在多种方法里选择最适合的那一个。以下是一些在面临抉择时的建议。

不要将数据存储在应用组件中

避免将 Android 组件(ActivityFragmentServiceBroadcastReceiver)作为数据源,相反,这些部分应当尽可能 “薄”,它们只应该用来与其他组件协调,以检索与其相关的数据集。每个应用组件存在的时间都很短暂,并且是不可控的,具体取决于用户与其设备的交互情况以及系统当前的整体运行状况。

减少对 Android 类的依赖

应用组件应该是唯一依赖于 Android 框架 SDK API(例如 ContextToast)的类。将应用中的其他类与这些类分离开来有助于改善可测试性,并减少应用中的耦合。

在应用的各个模块之间设定明确定义的职责界限

例如,请勿在代码库中将从网络加载数据的代码散布到多个类或软件包中。同样,也不要将不相关的职责(如数据缓存和数据绑定)定义到同一个类中。遵循推荐的应用架构可以帮助您解决此问题。

尽量少公开每个模块中的代码(高内聚,低耦合)

隐藏模块内部实现细节,对外只暴露接口,给未来版本迭代降低成本。

专注于应用的独特核心,将重复工作流程化

完整地表述应该是:

  • 将复杂的事情简单化
  • 将简单的事情标准化
  • 将标准的事情流程化
  • 将流程的事情自动化
  • 将自动的事情专业化

在编码领域就应该是:Do not repeat yourself! 不要一次又一次编写雷同的样板代码,对于一段代码/设计,第二次编写就要警觉,第三次编写就必须重构! 开发者的时间和精力应当集中放在让应用与众不同的方面。

考虑如何使应用的每个部分可独立测试

例如在处理网络/本地数据时,如果使用明确定义的 API 从网络获取数据,将会更容易测试在本地数据库中保留该数据的模块。如果将这两个模块的逻辑混放在一处,或将网络代码分散在整个代码库中,那么即便能够进行有效测试,难度也会大很多。

接口应当是主线程安全的

在 APP 开发中,接口的具体实现应当为自身负责,意味着 如果这是一个耗时操作,它自己应当进行线程切换。任何对外接口的调用都应当是 主线程安全 的。—— 对这一点我存疑,因为这样有可能造成不必要的线程切换成本。

保留尽可能多的相关数据和最新数据

也就是 OfflineFirstRepository,这样,即使用户的设备处于离线模式,他们也可以使用应用的功能。请记住,并非所有用户都能享受到稳定的高速连接——即使有时可以使用,在比较拥挤的地方网络信号也可能不佳。

设计良好的架构能带来哪些好处

  • 更高的开发效率
    • 提高整个应用可维护性、质量、稳定性。
    • 提高伸缩性,将代码分离从而降低冲突,使更多人、更大团队可以同时为代码库做贡献。
    • 有助于新人上手,统一的设计思路和代码规范能够极大降低学习成本。
  • 更好的软件质量
    • 更易于单元测试和集成测试。
  • 更快的分析问题
    • 依据明确定义的数据流、控制流,更有条理地分析 bug。

参考资料