
架构设计、Jetpack 与 Compose
高级岗位的架构题,本质上不是考你会不会背 MVVM、MVI、Clean Architecture 这些词,而是看你能不能在复杂业务里把状态、职责、依赖和变更成本控制住。
1. 为什么很多项目从 MVP 转向 MVVM?
参考答案
MVP 的核心问题通常不是理论不成立,而是在中大型项目里容易出现大量接口、样板代码和状态分散。MVVM 借助 ViewModel、响应式数据流和生命周期感知能力,更容易把页面状态集中管理,减少 Activity/Fragment 直接承载业务逻辑。
但不要把它回答成"MVVM 一定比 MVP 高级"。更准确的说法是:MVVM 在现代 Android 生态里和 Jetpack 套件协同更自然,所以维护成本通常更低。
面试官继续追问什么
MVVM的状态膨胀问题怎么处理?- 为什么有些团队又转向
MVI? - 架构分层多了以后,如何避免过度设计?
追问怎么答
- 状态膨胀要靠分层和拆域处理,不要把所有弹窗、列表、筛选和事件都堆进一个巨大的页面状态里。
- 团队转向
MVI,通常是因为状态来源太散、排查困难,希望用单向数据流把复杂度收回来。 - 避免过度设计的关键是按当前复杂度付费,能稳定解决问题即可,不要为了"看起来完整"提前抽太多层。
直接套用句式
"我理解从 MVP 转向 MVVM 不是因为它更新,而是因为页面复杂度上来以后,原来的回调和接口方式很难继续控住状态。我们更需要的是把页面状态和职责收回来,而不是单纯换个名词。"
2. ViewModel 为什么适合承载页面逻辑?
参考答案
ViewModel 的核心价值有三点:
- 跨配置变更保留页面状态
- 让 UI 容器与业务逻辑解耦
- 更适合作为状态流的汇聚点
它不应该变成"什么都往里塞"的大仓库。好的 ViewModel 更像页面状态协调者:接收用户意图、调用用例或仓库层、转换成可观察状态,再把一次性事件和稳定状态分开暴露给 UI。
面试官继续追问什么
- 为什么不建议在
ViewModel持有Activity或View引用? SavedStateHandle适合保存什么?ViewModel和Repository的职责边界怎么划?
追问怎么答
ViewModel生命周期往往比页面实例更长,持有Activity或View容易导致泄漏和职责混乱。SavedStateHandle适合保存少量关键恢复信息,比如页签、查询词、对象 ID,不适合塞大对象。ViewModel负责页面状态和意图处理,Repository负责数据获取与整合,前者别越界到底层细节,后者别反过来操作 UI 语义。
直接套用句式
"我会把 ViewModel 当成页面状态协调者,而不是业务大仓库。它负责收口页面意图和状态,真正的数据获取和存储细节我会继续放在更下层。"
3. LiveData、StateFlow、SharedFlow 怎么在页面架构里分工?
参考答案
一个比较稳的回答是:
StateFlow用来承载稳定页面状态,比如加载中、成功、失败、列表数据、筛选条件。SharedFlow用来处理一次性事件,比如弹 Toast、跳转页面、关闭弹窗。LiveData在旧项目或与现有框架兼容时仍然可以用,但在复杂状态流场景下,Flow体系组合能力更强。
很多项目里的坑都来自"状态和事件不分"。比如把一次性跳转事件放进 StateFlow,页面重建后可能重复消费。
面试官继续追问什么
- 为什么单次事件会产生"粘性问题"?
repeatOnLifecycle和launchWhenStarted有什么差别?- 页面多个数据源汇总时如何避免状态分裂?
追问怎么答
- 单次事件会"粘",是因为容器保留了上一次值,新观察者重新订阅时又会收到一次,本来只该执行一次的动作就重复了。
repeatOnLifecycle会在进入目标状态时启动、离开时取消并重建收集,更适合长期流;launchWhenStarted的边界通常没它清晰。- 多数据源时要先定义统一状态出口,让所有结果先归并再渲染,而不是每个源各自改一块 UI。
直接套用句式
"我在页面架构里会刻意把状态和事件分开,因为这两类东西生命周期不一样。稳定状态应该能被重复观察,一次性事件则必须防止重放。"
4. Room、WorkManager、Navigation 分别解决什么问题?
参考答案
Room解决的是本地数据库访问的可维护性问题,让SQL、实体、迁移、线程约束有更明确的结构。WorkManager解决的是"需要在系统约束下可靠执行的后台任务",比如延迟任务、重试任务、带网络条件的同步任务。Navigation解决的是复杂页面跳转、参数传递和回退栈管理问题,尤其适合中大型项目统一导航策略。
回答时别只说"官方推荐"。高级岗位更喜欢听到你知道这些组件分别解决了哪一类复杂度。
面试官继续追问什么
- 为什么
WorkManager不适合所有后台任务? - 数据库迁移如果失败,线上会有什么风险?
- 多模块项目里怎么避免
Navigation图过于集中?
追问怎么答
WorkManager适合可延迟、可重试、受系统约束的任务,不适合必须立刻执行、强实时反馈的交互场景。- 数据库迁移失败会直接导致崩溃、数据不可读,严重时还会让线上版本回滚和兼容都很难做。
- 多模块项目里不要把所有路由都堆在一个大图里,可以按业务域拆图,再在上层聚合。
直接套用句式
"我回答 Jetpack 组件时一般不会说'官方推荐',而是会先说它到底帮我控制了哪类复杂度。比如 WorkManager 控的是受系统约束的后台任务,Room 控的是本地结构化数据和迁移风险。"
5. 依赖注入为什么能提升工程可维护性?
参考答案
依赖注入的核心收益不是"少写 new",而是把对象创建和对象使用分离。这样做的直接好处是:
- 依赖关系更清晰
- 测试替换更容易
- 生命周期更容易统一管理
- 公共能力更适合下沉为可复用模块
Hilt/Dagger 的价值在于把这些依赖关系显式化、可验证化,减少手写装配带来的错误。
面试官继续追问什么
- 为什么过度注入也会让代码更难读?
- 单例、页面级、功能级依赖怎么划生命周期?
- 你们项目里最大的依赖治理问题是什么?
追问怎么答
- 过度注入会让对象来源过于隐蔽,读代码时看不出依赖从哪来,理解和排查成本都会上升。
- 单例适合全局共享、线程安全且代价高的对象;页面级依赖跟页面同生命周期;功能级依赖围绕某个流程或子域存在。
- 真正的依赖治理难点通常不是有没有框架,而是边界是否清晰、版本是否统一、公共能力是否被滥用。
直接套用句式
"我看依赖注入最大的价值,不是少写几个 new,而是把依赖关系显式化、可替换化。这样页面、模块和测试的边界都会更清楚。"
6. 页面状态复杂时,MVI 或单向数据流为什么更容易控住局面?
参考答案
当页面存在多个数据源、多个用户动作、多个异步结果,以及复杂的加载态和失败态时,最大的风险不是"代码写不出来",而是状态来源太多、修改点太散,最后谁都不敢动。
单向数据流的价值在于把流程收敛成:
- 用户产生意图
- 逻辑层处理意图
- 输出新的统一状态
- UI 只根据状态渲染
这样更容易排查、回放和测试。但它也有代价,比如样板代码增多、状态建模要求更高,所以不是所有页面都要强行上完整 MVI。
面试官继续追问什么
- 如何避免一个巨大的
UiState越来越难维护? - 为什么有些页面适合
MVVM,有些更适合MVI? - 你如何设计"加载中 + 部分失败 + 局部重试"这种复杂状态?
追问怎么答
- 巨大的
UiState要靠子状态拆分和领域分层来控制,不要把所有局部细节都平铺进一个总状态。 - 交互简单、状态变化少的页面用
MVVM往往就够;多数据源、多事件、多失败态页面,MVI更容易收口。 - 这类复杂状态要先分全局和局部,再给每个区域定义自己的失败和重试语义,避免一个错误把整页状态打乱。
直接套用句式
"我选 MVVM 还是 MVI,不会按喜好来,而是看页面状态复杂度。如果页面已经有多个数据源、多个交互入口和复杂失败态,我更倾向用单向数据流把状态收口。"
7. Compose 和传统 View 的本质区别是什么?
参考答案
传统 View 更偏命令式:你通过更新控件属性、调用方法去改变界面。Compose 更偏声明式:你描述当前状态下 UI 应该长什么样,框架负责在状态变化后重新计算和刷新。
这意味着 Compose 的核心不只是"新写法",而是状态驱动 UI。你如果没有先把状态设计好,到了 Compose 时代问题只会暴露得更明显。
面试官继续追问什么
- 为什么声明式 UI 更依赖状态建模?
Compose混合旧View体系时,最常见的问题是什么?- 什么时候不应该急着全量迁移到
Compose?
追问怎么答
- 声明式 UI 是"状态决定界面",状态建模一乱,重组范围和 UI 行为就会一起乱。
- 混用时最常见的是生命周期、状态同步、滚动和输入焦点等边界没处理好,导致两套体系互相影响。
- 老项目收益不明显、团队不熟或基础组件没铺平时,不必急着全量迁移,先从高收益局部试点更稳。
直接套用句式
"我理解 Compose 的本质不是新语法,而是声明式状态驱动 UI。所以如果状态建模没做好,迁到 Compose 只是把原来的问题更早暴露出来。"
8. 什么是重组?怎么避免无意义重组?
参考答案
重组可以理解为:当 Compose 观察到某些状态变化后,会重新执行相关组合函数,计算新的 UI 描述。重组本身不是坏事,关键是它是否发生在该发生的范围内。
避免无意义重组,常见做法包括:
- 状态下沉到最小需要感知的范围
- 使用稳定的数据结构
- 不在组合函数里做重逻辑
- 避免把频繁变化的大对象整体传下去
面试官继续追问什么
remember和rememberSaveable的区别?derivedStateOf适合什么场景?- 为什么有时看起来变的是一个字段,却导致整块 UI 重组?
追问怎么答
remember只在当前组合存活期间记住值;rememberSaveable还能在配置变更或恢复时保留可保存状态。derivedStateOf适合从多个输入派生一个高频读取、低频真正变化的结果,减少无意义计算和重组。- 因为你可能把整个大对象作为输入往下传了,只要对象整体被认为变了,依赖它的整块 UI 都可能跟着重组。
直接套用句式
"我看重组问题时,重点不在于'有没有重组',而在于'重组范围是不是合理'。因为重组本身是正常机制,真正要避免的是无意义的大范围连带刷新。"
9. LaunchedEffect、DisposableEffect、SideEffect 怎么区分?
参考答案
LaunchedEffect适合启动与组合生命周期绑定的协程副作用,比如进入页面后拉数据。DisposableEffect适合需要注册和清理的副作用,比如监听器、生命周期观察者。SideEffect适合在成功重组后,把当前组合状态同步到外部对象。
面试时的关键点是:副作用 API 的本质,是让"声明式 UI"与"不可避免的命令式世界"安全协作。
面试官继续追问什么
- 为什么不能在组合函数里直接随便发请求?
key变化会怎么影响副作用重新执行?- 页面退出时如何确保资源释放?
追问怎么答
- 组合函数会被反复执行,直接在里面发请求会让副作用重复触发,行为不可控。
key是副作用的重启条件,key一变,对应协程或监听通常就会取消并重新建立。- 需要注册和释放的资源要放到明确有清理时机的副作用 API 里,比如
DisposableEffect,而不是散落在 UI 代码中。
直接套用句式
"我回答 Compose 副作用时,一般会强调一点:声明式 UI 不等于没有副作用,而是副作用必须放到受控的生命周期里执行。"
10. 高级岗位讲架构,面试官真正要听什么?
参考答案
他真正想听的通常不是你背了多少模式,而是这几个问题:
- 复杂度是怎么被拆开的?
- 状态是怎么被收敛的?
- 依赖是怎么被约束的?
- 这套方案为什么比你们过去的方式更稳?
- 代价是什么,哪些地方你刻意没有过度设计?
所以回答架构题时,尽量别停在"我们用了 MVVM + Hilt + Repository"。更好的说法是:
"我们原来页面逻辑散在 Fragment、Adapter 和回调里,导致状态不可控。后面把状态集中到 ViewModel,用 StateFlow 表达稳定状态,用事件流表达一次性动作,再通过依赖注入把数据源切换和测试替身成本降下来。收益是页面迭代稳定性更高,代价是前期状态建模更严格。"
这才像高级工程师的回答。
面试里可以这样收口
"所以我讲架构题时,一般不会停在'我们用了什么模式',而是会继续讲:原来哪里失控、为什么要这么改、改完后复杂度是怎么收回来的,以及代价是什么。"