你搞得懂这 15 个 Android 架构问题吗

0 阅读13分钟

v2.jpg

大家都是 Android 开发老手(现在还哪有新手?),应该知道架构设计能力是区分资深与普通开发者的核心标志。

下面我问 15 个问题,这些问题直击 Android 架构的关键痛点与实践精髓,它们既是面试高频考点,更是日常开发中避坑指南。

1. 为什么 MVVM 在项目后期会变得臃肿

MVVM 之所以长盛不衰,核心是与 Android 的生命周期感知组件天然契合——ViewModel 能无缝处理配置变更、暴露可观察状态,还能清晰分离UI渲染与业务逻辑。理论上,它完美平衡了简洁性与结构性,堪称理想架构模式。

但现实往往骨感。

很多团队初期思路清晰,后期却把 ViewModel 当成了“万能容器”:网络请求悄悄塞进来,数据库操作偷偷加进去,数据映射逻辑越堆越多,最后连导航决策也往里塞。原本的状态持有者,不知不觉变成了包揽一切的服务层,MVVM 架构就此崩塌。

可能很多小伙伴一直没有明白“负责协调 UI 状态”到底什么意思?简单的来讲,就是 UE 上的逻辑。即从仓库获取数据后,如何将这些数据显示在UI上,这个逻辑以及UI的状态,正是 ViewModel 职责所在!

各位老手应该都懂:MVVM 的核心是“职责边界”。ViewModel 只该负责协调 UI 状态,业务规则交给用例或领域服务,数据获取交给仓库(Repository),数据映射交给专门的转换器。把 ViewModel 精简到只做协调工作,MVVM 才能真正可持续。

关键认知是:MVVM 不是模式本身有问题,而是团队把它当成了“顺手放东西”的便利贴,才导致架构失控。

2. Clean Architecture 在 Android 项目中到底是什么

1.png

很多开发者觉得,建个 datadomainpresentation 文件夹,就叫 Clean Architecture 了。但这种只重表面结构的做法,根本没 get 到其精髓。

Clean Architecture 的核心是“依赖方向”:内层必须独立于外层。领域逻辑不该知道数据来自 RetrofitRoom 还是模拟实现,更不该依赖 Android 框架类——领域层要以最纯粹的形式承载业务规则。

在 Android 项目中,这意味着:在领域层定义仓库接口,在数据层实现接口;UI 层只和领域用例交互,绝不直接依赖具体的数据实现。

开发老手的解读是:Clean Architecture 不是为了分层而分层,而是为了保护核心业务逻辑不被外界变化影响。

框架会迭代、库会替换、UI 范式会变迁,但业务规则(可以理解为 UE 定义的逻辑)往往是稳定的——Clean Architecture 就是要把这份稳定性隔离出来。

业务规则...稳定吗?

上面这段话,可能很多开发者会觉得:业务规则是不稳定,可能我的库一辈子不会替换,但是产品逻辑一直在变换。

这里的业务规则稳定,其实是另一种概念,我举个例子,假设产品设计了一个产品,当开发评估实现过程的时候,可能考虑到了 IOS,Windows,Web,Android 等不同平台,针对这些不同平台,它们的业务逻辑是统一的,但是到研发手里,UI,数据存储等这些技术细节就是变化的。

即使只考虑 Android,我们也会遇到很多 Bug,这些 Bug 也会让技术细节有特别多的改动,因为技术细节就是为产品服务的,其主要目的就是为了实现产品逻辑的稳定。

3. 现代 Android 应用中,业务逻辑该放在哪里

嘿,这个看似简单的问题,最能体现架构成熟度,也最能体现资深与普通开发者的差距!

初级开发者常把业务逻辑写在 ActivityFragment 里;稍进阶一点的,会把所有逻辑都移到 ViewModel。但这两种做法都有隐患——它们把业务规则和 Android 生命周期类强绑定,后续维护只会越来越难。

如果是个人的 Demo 项目,为了简单的解决一个小问题,后续没有什么迭代更新,这么做是没有任何问题的 —— 够快才能抓住机会!

而开发老手的选择是:引入用例(UseCase)。每个类对应一个独立的业务操作,比如CalculateShippingCostUseCase(计算运费)、SubmitLoginUseCase(提交登录),把验证规则、仓库调用、结果处理都封装在里面。

ViewModel 只负责调用用例,再把结果以 UI 状态的形式暴露给视图。

这样做的好处很明显:业务逻辑脱离 Android 依赖,可独立测试;能在多个页面复用;ViewModel 也能专注于 UI 协调,不用操心领域逻辑。

一个实用的判断标准:如果明天 Android 框架消失了,你的业务逻辑依然能独立运行、逻辑自洽,那它的位置就对了;反之,就是放错了地方。

仔细想想,UseCase 大部分代码和 Android 是没有关系的,它有自己的输入以及针对 UI 状态的输出。

4. 什么时候该从单模块应用迁移到多模块架构

2.jpg

早期应用适合单模块结构——搭建快、理解成本低,小团队用着刚刚好。但随着代码库扩大,问题会逐渐暴露:构建时间越来越长,不相关功能耦合越来越紧,新开发者上手越来越难。

多模块架构的核心价值是“划清边界”:可以把功能拆成独立模块,核心工具、设计系统、网络层、领域逻辑则放在共享模块中。这样能缩小编译范围,明确代码所有权,避免团队协作时互相干扰。

开发老手不会为了“显得高级”而盲目模块化。

只有当出现明确痛点——比如构建太慢、团队协作冲突频繁、需要隔离功能所有权时,模块化才是合理选择。否则,过早模块化只会增加不必要的复杂度。

架构决策从来不是选“理论上最好的”,而是选“能解决当前瓶颈的”,即局部最优解即可!

5. 仓库(Repository)的真正作用是什么

很多项目里的 Repository,只是 API 服务的“薄包装”——调用 Retrofit 返回结果,顶多再加个数据库查询。这种表层实现,完全浪费了仓库的架构价值。

仓库的核心职责是“抽象数据源”,给应用其他部分提供统一接口。它要决定数据来自缓存、网络还是本地存储,可能还要组合多个数据源、做数据转换、执行一致性规则。

举个例子:仓库可以先返回缓存数据,保证 UI 响应迅速,同时后台从网络拉取最新数据并更新缓存。而 ViewModel 完全不用关心这些细节,只要观察仓库的输出就行。

资深开发者把仓库当成“决策者”,而不是“传递者”。如果一个仓库只是简单转发请求,那它其实没有很大必要存在。

6. Compose 如何改变架构思维

3.png

Compose 的核心变革,是把 UI 开发从“命令式更新”变成了“声明式渲染”。

开发者不用一步步告诉视图该做什么,而是描述“特定状态下 UI 该长什么样”。

这一变革让“状态管理”成为架构核心——UI 变成了状态的函数。如果状态可预测、不可变,UI 的表现也会变得可预测。

开发老手们通常为每个独立的 UI 功能定义一个单一的 UI 状态对象(通常使用 data class),包含所有必要属性。

状态变化会触发重组,事件从 UI 向上传递,状态从 ViewModel 向下流动,形成单向数据流。

现代化的 UI 开发,一句话讲:状态导致 UI 变化,UI 通过事件导致状态变化!

Compose 不强制要求新架构,但会更快暴露旧架构的缺陷。如果团队无视单向数据流,手动触发 UI 更新或在多个地方存储可变状态, bug 会很快找上门。

架构必须为屏幕状态提供“单一数据源”,才能适配 Compose 的开发模式。

7. 2026年了,该用 Flow 还是 LiveData

其实这个问题,在三年前就应该下结论——Flow 一把梭哈!

LiveData 在早期 Android 架构中功不可没,但如今 Kotlin Flow 已经成为更灵活、更强大的响应式组件。

Flow 与协程无缝集成,支持丰富的转换操作符,还能在应用所有层通用。和 LiveData 不同,它默认不绑定 Android 生命周期,更适合用在领域层和数据层。

开发老手的实践是:内部用 Flow 传递数据,在 UI 边界处用生命周期感知的作用域收集数据。

Compose 中,以生命周期感知方式收集 Flow 已经变得非常简单(一个函数搞定——collectAsState)。

这个选择不是要弃用 LiveData,而是要选一个能在整个架构中支持异步管道的流抽象——Flow 显然更胜任这个角色。

当然,选择 Flow 还有另一个很重要的原因:Flow 是纯 Kotlin 实现的,这是跨平台开发的唯一选择。

毕竟,夫人......哦不是,开发者们,你也不想再跨平台的时候,重新写一遍数据流吧!

8. 如何防止 ViewModel 变得臃肿

这是 Android 应用中最常见的架构问题之一。

在 #1 的问题中,提到过。

ViewModel 容易膨胀,核心是因为它太“好用”了:能存活于配置变更、可访问协程、处于 UI 和数据之间的关键位置。久而久之,各种职责都会堆过来,最后变得臃肿不堪。

开发老手的应对方法是“主动向外剥离逻辑”:复杂业务操作移到用例,数据映射移到专门的映射器类,数据组合逻辑放到仓库,导航决策可以封装成事件,由导航处理器处理。

一个健康的 ViewModel,读起来应该像“状态协调器”;如果读起来像“小型后端服务”,说明它已经超出了应有的职责范围。

9. 大型 Android 应用的导航该如何设计

小型应用可以直接用 Navigation 组件,inline 定义 FragmentCompose 路由就行。但大型应用的导航逻辑,往往需要更高层次的抽象。

如果 ViewModel 直接调用 UI 框架的导航 API,会造成强耦合。资深团队的做法是:ViewModel 只发送导航意图或事件,UI 层解析这些事件并执行导航操作。

有些团队会引入导航接口或协调器模式,集中管理导航决策。这种方式让导航逻辑可测试,还能避免业务逻辑直接引用 UI 目标页面。

导航看似是 UI 层面的事,但在复杂应用中,它会成为重要的架构边界。

等等,什么是架构边界?

一件事,该谁干——就是边界。

10. 如何处理依赖注入,不让框架主导架构

Hilt 这类框架能减少模板代码,但也容易让人走捷径。如果过度依赖注解和自动注入,依赖关系可能会无意间跨越架构边界。

开发老手的原则是:先设计架构,再把 DI 框架当成“组装工具”。领域层依赖接口而非实现,数据层提供具体实现,UI 层只依赖抽象。

如果切换 DI 框架需要重构整个代码库,说明架构和框架耦合太紧了。

框架应该支持架构设计,而不是定义架构设计。

我个人最喜欢的 DI 框架其实是 Koin

11. 如何设计离线优先或缓存感知系统

4.jpg

离线支持不该是“事后补充”的功能——它会影响整个应用的数据流转逻辑。

有经验的开发者通常把缓存逻辑放在仓库层:由仓库决定何时返回缓存数据、何时刷新数据、如何合并多源数据。它可能会暴露一个 Flow,先发送缓存数据,再发送更新后的新数据。

UI 完全不用关心数据来自网络还是本地磁盘,只需响应状态变化。这种设计让离线逻辑集中化,避免多个页面重复写缓存代码。

早期就为不稳定网络设计架构,能省去后期大量重构工作。

从这个问题就可以看出,为什么现代化的开发都喜欢使用 Repository 去处理数据,而不是直接调用 Retrofit 接口。

12. 错误该如何在架构各层传递

直接向上层抛出原始异常,虽然简单但很混乱——网络错误、解析失败、数据库问题等技术细节,会泄露到 UI 层。

一个好的系统的做法是:把技术错误转化为领域级错误模型。比如仓库不会抛出 IOException,而是发送DomainError.NetworkUnavailable(网络不可用)或DomainError.Timeout(超时),ViewModel 再把这些领域错误映射成用户友好的提示信息。

这种方式让各层“说自己的语言”:数据层处理技术细节,领域层定义业务含义,UI 层负责展示。错误处理从此变得可预测,而不是杂乱无章。

13. 什么时候领域层才是真正必要的

不是所有应用都需要完整的领域层(包含用例和实体)。过早引入领域层会拖慢开发速度,还会增加不必要的间接性。

老手们会观察“信号”再决定是否添加领域层:如果业务规则复杂、需要在多个功能中复用、或者需要脱离 Android 独立测试,领域层就很有价值;如果应用只是简单展示远程数据,几乎不需要数据转换,那更简单的结构就足够了。

好的架构会随产品一起演进,很少能在第一天就设计得尽善尽美(当然也没必要做,提前优化也是一种负优化)。

14. 如何确保架构从一开始就支持测试

测试难度往往能反映架构缺陷。

如果写个单元测试都需要启动 Activity,或者 mock 半个 Android 框架,说明架构边界设计错了。

一个合理的架构,会把业务逻辑隔离在纯 Kotlin 类中,分离接口与实现,核心层避免直接依赖 Android 框架。这样一来,单元测试就能快速、可靠地运行。

这也正是为什么 #7 问题中,推荐使用 Flow 的原因,Flow 不依赖 Android,纯 Kotlin 就能测试!

测试不是在架构设计完成后才添加的,而是关注点分离良好的自然结果。

15. 现在 Android 团队最容易犯的架构错误是什么

最常见的错误是“盲目照搬模式,却不理解其目的”。很多团队照搬 Clean Architecture、创建多个模块、引入复杂的状态容器、添加多层抽象,只因为他们觉得“这才是高级架构该有的样子”。

但不必要的抽象会拖慢开发速度,让新成员困惑。

最好的架构不是分层最多的,而是能清晰分离职责,同时又足够简单、让团队能轻松理解的。

资深开发者优化的是“清晰度、可扩展性和可维护性”,而不是用各种模式名称去打动别人。