这篇适合怎么用
本文聚焦Kotlin 技术栈下的 MVVM:面试官常问的落点、追问方向,以及你可以直接替换项目名词套用的句式。你不需要先读其他系列文章,也能直接使用本文的答题框架。
161721767
如果想补代码闭环,可看上一篇 《Kotlin MVVM 实战入门:从分层到状态闭环》。如果你已经会写,本文重点是把它讲成「复杂度怎么被收敛」,而不是背框架名。
适合读者:有 Kotlin 基础、写过几个 Android 页面、想系统化准备 MVVM 面试表达的工程师。不要求精通协程和Flow,但建议至少知道ViewModel、协程作用域和状态流的基本概念。
1. 你怎么理解 Kotlin 项目里的 MVVM?
先看一张最小流向图:
MVVM 的重点不是三个文件夹,而是流向清晰:View发用户意图,ViewModel组织异步并更新状态,Repository处理远程/本地数据,最后由StateFlow/UiState驱动 UI 渲染。
参考答法
「MVVM在我这儿首先是状态与职责问题:View只负责展示和把用户操作交给ViewModel;ViewModel在生命周期内保留页面状态、用协程组织异步;Model这一侧我通常拆成Repository或再下一层的数据源,避免Activity里堆接口回调。Kotlin 带来的好处是协程 +Flow让异步链路更像顺序代码,但模式本身不自动解决复杂度,关键还是状态是否收口、事件是否分离、边界是否清楚。」
面试官在听什么
- 你有没有把 MVVM 说成「三个文件夹名字」
- 你是否知道异步与生命周期在 Kotlin 里怎么对齐
怎么说才加分
- 主动提「状态 vs 一次性事件」分离。
- 用一句项目话收尾:「我们当时主要解决的是状态散在
Fragment和Adapter里难排查的问题。」 - 不要把亮点说成「用了 MVVM」,要说「原来什么复杂度被它收敛了」。
2. ViewModel 为什么能处理配置变更?你在项目里怎么用它?
参考答法
「配置变更时Activity/Fragment会重建,但ViewModel是按作用域缓存的,所以适合保存与当前页面强相关、又与具体View实例无关的状态。我会让ViewModel持有StateFlow表达的UiState,页面只订阅;作用域销毁时viewModelScope会取消协程,避免页面没了请求还在跑。」
追问怎么接
- 为什么不要持有
Activity?
「ViewModel可能比某次Activity实例活得更久,持有引用容易泄漏,也让测试和职责划分变糊。」 - 为什么旋转后还能拿到同一个 ViewModel?
「不是ViewModel自己不会销毁,而是配置变更时ViewModelStore被保留下来;新的Activity/Fragment会通过同一个ViewModelStoreOwner重新拿到原来的ViewModel实例。」 SavedStateHandle用来干什么?
「少量要进进程恢复栈的状态,比如选中 tab、搜索词;大对象仍走自己的存储或重新拉取。」- 什么不该放进
ViewModel?
「不放View、Activity Context、大缓存、长生命周期单例,也不直接创建网络客户端。ViewModel是页面状态协调者,不是万能仓库。」
直接套用句式
「我把ViewModel当页面状态协调者:跨旋转保留该保留的;该进系统恢复链路的轻量状态用SavedStateHandle,该持久化的数据交给下层仓库。」
3. 为什么很多 Kotlin 项目用 StateFlow 承载页面状态?
参考答法
「StateFlow是热流,始终持有当前值,适合表达「此刻页面长什么样」;普通Flow默认是冷流,每次collect都会重新执行上游逻辑,更适合描述一次数据处理管道。和LiveData比,StateFlow与协程生态统一、组合操作更直观。StateFlow本身不感知生命周期,生命周期管理由 collect 端负责,比如用repeatOnLifecycle或collectAsStateWithLifecycle把收集范围绑到页面可见生命周期。」
追问怎么接
- 单次事件为什么容易踩坑?
「StateFlow会保留最后一个值,重建订阅可能重放;所以 Toast、导航我会用SharedFlow或明确的事件通道,和稳定状态分开,不再靠SingleLiveEvent这类老写法硬兜。」 - StateFlow 和 SharedFlow 本质差别是什么?
「StateFlow是永远有当前值的热流,关注的是「当前状态是什么」;SharedFlow更像广播流,关注的是「发生过什么事件」。所以页面状态天然适合StateFlow,导航、Toast、埋点这类一次性动作更适合SharedFlow。」 - SharedFlow 的事件页面重建后会重放吗?
「一次性事件用SharedFlow配合replay = 0,页面重建后不会重放;如果误配replay = 1且没有重置事件,重建页面就可能重复收到旧事件。」 StateFlow更新要注意什么?
「用update或基于旧值copy,避免非原子读写;多协程写入时要清楚是否在抢同一状态。」- 为什么不直接在
Fragment里 collect?
「直接 collect 不一定立刻崩溃,但页面退到后台后可能仍持续收集,造成资源浪费和无效更新。传统 View 我会用repeatOnLifecycle(Lifecycle.State.STARTED),Compose 用collectAsStateWithLifecycle,把收集范围限定在可见生命周期内。」 - StateFlow 为什么不像 LiveData 一样自动感知生命周期?
「因为Flow是 Kotlin 协程库的一部分,不是 Android 专属组件。生命周期管理被放在 collect 端完成,所以 Android UI 层要用repeatOnLifecycle或collectAsStateWithLifecycle来接入生命周期。」
4. 协程在 MVVM 里扮演什么角色?viewModelScope和lifecycleScope怎么分工?
参考答法
「业务异步我默认放在ViewModel里用viewModelScope启动,这样与页面作用域一致,销毁即取消。lifecycleScope更适合紧贴 UI 的一次性动画、延迟与页面级副作用;若逻辑会跨配置变更,仍应沉到ViewModel。」
追问怎么接
- 取消不生效?
「要看有没有挂起点、是否在阻塞调用上;结构化并发是否把子任务挂在同一coroutineContext。」 - 怎么确认取消真的生效?
「调试时可以在协程finally里打日志,看页面退出或ViewModel清理后是否执行;测试里可以用runTest+TestDispatcher控制调度,断言任务取消后的状态变化,而不是只靠肉眼看请求有没有返回。」 - 为什么不全局 launch?
「难追踪、难取消、难测,线上问题不好归因。」 - 多个子协程里一个失败,其他会怎样?
「结构化并发默认会把失败向上传播,一个子协程异常可能导致同级任务被取消。需要隔离失败时,可以用supervisorScope或SupervisorJob,但要明确异常由谁处理,不能只是为了“不崩”而吞错误。」 runCatching包协程请求可以吗?
「可以用,但要小心别吞掉CancellationException。我会在onFailure里显式判断CancellationException并重新抛出,避免取消被当成普通失败态展示;如果团队不偏好runCatching,用try/catch更直观。」- 重复点击刷新怎么办?
「先定策略:忽略重复、取消上一次,或者串行排队。高级一点不是写个launch,而是说清楚并发结果谁覆盖谁。」
5. Repository 和 ViewModel 的边界在哪?
参考答法
「Repository面向数据获取与策略:远程、本地、缓存、合并、重试、降级;ViewModel面向页面语义:加载态、错误文案是否展示、列表排序是否跟 UI 状态走。边界不清时,常见结果是Repository里混入弹窗逻辑,或者ViewModel里写 SQL、拼缓存。」
怎么说才加分
- 提一句测试:「
Repository用假数据源替换,ViewModel做单测时不用起真Activity。」 - 提一句取舍:「简单页面不一定要抽
UseCase,复杂业务规则复用、组合变多时再加。」 - 提一句边界判断:「如果逻辑换页面后仍基本复用,优先下沉到
Repository/UseCase;如果强绑定当前页面交互语义,就留在ViewModel。」 - 提一句分层细节:「
DataSource负责具体数据来源,比如 Retrofit、Room、DataStore;Repository负责数据策略,比如缓存、降级、合并和最终返回什么结果。」
UseCase 什么时候值得加?
- 多个
Repository组合,单个ViewModel里编排会变重。 - 业务规则需要复用,比如权限判断、风控逻辑、排序过滤、价格计算。
- 同一动作会被多个入口触发,希望集中维护规则和测试。
- 如果只是简单转发
Repository,UseCase往往只是增加层级。
6. 单元测试 / 可测性怎么一句带过又显得做过?
参考答法
「我会把网络和数据源藏在接口后面,ViewModel测时用FakeRepository驱动各种返回;协程用TestDispatcher控制时间。面试里我不展开每个 API,但会强调可替换依赖是选 MVVM + DI 的实际原因之一。」
可以顺手补一句:「状态测试重点看输入动作后UiState怎么变化,事件测试看 Toast / 导航是否只发一次;不要只测有没有调用某个方法。」
再补一句落地细节:「我一般用runTest+TestDispatcher控制调度,再用 Turbine(一个专门测试Flow的库,或直接收集StateFlow转列表断言)验证状态序列,避免真实时间等待造成测试不稳定。」
7. 和 MVI、Clean 怎么一句话共存?
参考答法
「页面复杂度还没到多源多事件难排查时,MVVM+ 明确状态模型就够用;当页面状态来源很多、事件链路复杂、需要完整记录状态迁移过程时,MVI的单向数据流优势会更明显。Clean是更外层的依赖方向约束,可以渐进引入,不必一次上全。」
(现场答一屏即可:先说当前复杂度,再说为什么此刻不必上更重方案。)
可补一句立场:
「我不会为了用MVI而用。大多数页面用MVVM+ 单向状态流已经足够,复杂页再升级更重模式。」
8. Jetpack 在 MVVM 里常用哪些?
参考答法
「我不会把Jetpack只背成组件清单,而是按它们解决的问题来讲:ViewModel解决配置变更下的页面状态保留;Lifecycle解决生命周期感知;LiveData/StateFlow解决状态分发;Room解决本地结构化数据;Paging解决分页加载和分页状态管理;DataStore解决轻量配置持久化;WorkManager解决受系统约束的可靠后台任务;Navigation解决页面跳转和回退栈管理;Hilt解决依赖创建、作用域管理和测试替换。」
面试官在听什么
- 你是否知道每个组件解决的边界问题,而不是只会列名字。
- 你是否能把
Jetpack和MVVM的职责接起来:状态归ViewModel,数据归Repository,生命周期由Lifecycle约束。 - 你是否知道不是所有项目都要一次性上全家桶,复杂度和团队成本也要考虑。
追问补一句:列表场景常见Paging+cachedIn(viewModelScope),轻量配置常见DataStore+Flow,它们都可以自然接到ViewModel的状态流里。
9. LiveData 还会不会被问?和 StateFlow 怎么答?
参考答法
「会。很多老项目、Java 项目或历史模块里仍然有LiveData。它最大的价值是生命周期感知:只有STARTED/RESUMED这类活跃状态的观察者才会收到更新,LifecycleOwner销毁后会自动解除普通observe(owner)的绑定。现代 Kotlin 项目里我更倾向StateFlow,因为它和协程生态统一、组合操作更自然;但面试里我会说明:LiveData不是不能用,而是要清楚它的语义和迁移边界。」
追问怎么接
- LiveData 怎么监听?
「常规写法是liveData.observe(viewLifecycleOwner) { ... }。在Fragment里不要用this当LifecycleOwner,要用viewLifecycleOwner,避免 View 销毁后还继续更新旧视图。」 - 为什么 LiveData 不更新?
「常见原因有:原地改了同一个对象但没有重新setValue;在后台线程调用了setValue;postValue连续多次只观察到最后一次;观察者还没进入活跃生命周期;或者用了错误的LifecycleOwner。」 - setValue 和 postValue 区别?
「setValue必须在主线程调用,并向活跃观察者分发;postValue可在后台线程调用,会切到主线程异步分发,短时间连续postValue可能合并,只收到最后一个值。」 - LiveData 注意事项?
「不要把一次性事件直接塞进LiveData当状态;observeForever必须手动移除;状态对象最好不可变更新;复杂 Kotlin 新模块优先考虑StateFlow/SharedFlow。」 - LiveData 原理一句话怎么说?
「它内部维护当前版本号和观察者包装对象,数据更新时只向活跃生命周期的观察者分发,并用版本号避免同一个观察者重复消费旧值。」
直接套用句式
「如果是老项目,我会尊重已有LiveData体系,把生命周期和事件语义处理好;如果是新 Kotlin 模块,我更倾向StateFlow + SharedFlow,因为状态、事件、协程取消和测试会更统一。」
10. Compose 时代 MVVM 还重要吗?
参考答法
「重要。Compose解决的是 UI 描述方式,MVVM解决的是状态管理和职责划分。Compose 是状态驱动 UI,反而让ViewModel + StateFlow + collectAsStateWithLifecycle()这条链路更自然:状态在ViewModel收口,组合函数只读取状态并发出用户意图,不在组合里直接发请求或保存业务状态。」
追问怎么接
- Compose 会替代 ViewModel 吗?
「不会。remember/rememberSaveable更偏组合内或可保存 UI 状态,ViewModel负责页面级状态协调、异步任务和跨配置变更保留。」 - Compose 里事件怎么处理?
「稳定状态用StateFlow,一次性事件仍要单独建模,比如SharedFlow或事件通道,不要因为换成 Compose 就把状态和事件混在一起。」
11. MVVM 在使用中的痛点,怎么答不空
参考答法(30 秒)
「MVVM的痛点通常不在模式本身,而在业务变复杂后:ViewModel容易膨胀、UiState字段越来越散、一次性事件语义容易混到状态里、并发请求会相互覆盖结果、Repository和 UI 边界会漂移。我的做法是把这几件事制度化:状态/事件分离、并发策略先定、边界按职责守住、复杂页再加UseCase,简单页保持最小分层。」
面试官在听什么
- 你是否讲得出“用起来哪里会失控”,而不是只讲优点。
- 你有没有稳定处理状态、事件、并发和边界的工程习惯。
- 你是否知道
UiState也可能膨胀:几十个字段、到处copy(),最后状态对象本身变成维护成本。 - 你是否知道
ViewModel也可能膨胀:加载、搜索、刷新、上传、埋点、弹窗判断全塞进去,最后变成新的“上帝对象”。
UiState 膨胀怎么处理
- 页面状态按区域拆成子状态,再由总
UiState聚合。 - 临时输入、局部展开态等纯 UI 细节,不一定都塞进全局
UiState。 - 复杂页面先建模状态关系,避免边写边往
data class里堆字段。
ViewModel 膨胀怎么处理
- 业务规则复用或组合变多时抽
UseCase。 - 复杂状态迁移可以引入 reducer / 状态处理器,让状态变化有固定入口。
- 数据转换和策略不要都堆在
ViewModel,该下沉到Repository或 mapper 的就下沉。
如果面试官追到列表页分页、筛选、刷新状态设计,可以按FilterState + PagingData + RefreshState + UiEffect拆,不要把所有东西都塞进一个巨大UiState。
可直接套用的量化句式
- 「改造后列表页状态错乱类问题从每周
X次降到Y次。」 - 「线上页面首屏可交互时间
P95从A ms降到B ms。」- 这里的
P95可以理解为:按耗时从快到慢排序后,95% 用户都不超过这个耗时,比平均值更能反映大多数偏慢用户的体验。
- 这里的
- 「相关页面
ANR/ 崩溃率在两周内从A%降到B%。」
如果被追问“数据怎么来的”,可以补这一句:
「这些数字来自 Crashlytics / 线上埋点的改造前后同口径对比(通常看两周窗口),MVVM 本身不是魔法,关键是把状态收口到UiState后,散落在多个LiveData/View的并发改写问题更容易被消除和验证。」
注意:没有真实数据就不要硬编数字。可以说「当时目标是把 P95 降到某个范围」,或者「内部复盘时观察到某类问题明显减少,但没有做严格归因统计」。
常见反模式(面试主动提 = 加分):
ViewModel持有Activity、View或短生命周期Context。View直接调用Repository,把页面逻辑和数据策略搅在一起。- 把一次性导航、Toast 当成稳定状态保存,导致重建后重复消费。
- 多协程直接读写
StateFlow.value,不用update,产生状态覆盖。 try/catch或runCatching吞掉CancellationException,把取消当失败态展示。
12. 面试官最喜欢听到的关键词
回答 MVVM 时,如果能自然带出这些词,通常比单纯介绍ViewModel、Repository、StateFlow更能体现工程经验:
- 状态收口
- 状态与事件分离
- 单向数据流
- 生命周期感知
- 结构化并发
- 可替换依赖
- 职责边界
- 配置变更恢复
- 状态驱动 UI
- 可测试性
13. 面试现场收口模板(可直接背骨架)
「我们 Kotlin 化之后,页面侧用ViewModel+StateFlow收口展示状态,异步走协程和Repository;一次性动作用SharedFlow/ 事件流和状态分开,避免旋转重放。生命周期收集用repeatOnLifecycle或collectAsStateWithLifecycle,避免不可见页面继续消费 UI 更新。收益是迭代和排查更顺,代价是前期要把UiState建模想清楚,不能边写边堆字段。」
14. 本篇高频追问速查
| 方向 | 高频追问 | 面试官在考什么 |
|---|---|---|
| 生命周期 | repeatOnLifecycle解决什么问题 | 生命周期感知与资源浪费 |
| ViewModel | 为什么旋转后状态不丢 | ViewModelStore与作用域 |
| 流 | LiveData、StateFlow与SharedFlow分工 | 状态建模与事件语义 |
| 协程 | 取消、阻塞、异常隔离、重复请求策略 | 结构化并发与稳定性 |
| Jetpack | 常用组件、Paging、DataStore各自解决什么问题 | 组件边界与场景选择 |
| 边界 | RepositoryvsDataSource、UseCase什么时候值得加一层 | 分层职责与复杂度控制 |
| Compose | Compose 时代还需不需要 MVVM | UI 描述和状态管理的边界 |
| 落地 | 列表页 + 分页 + 筛选状态怎么合并 | 复杂页面状态拆分 |
15. 白板手写速记(可选)
class XViewModel : ViewModel() { private val _state = MutableStateFlow(UiState()); val state = _state.asStateFlow(); fun onAction() { viewModelScope.launch { ... } } }
相关推荐
《Android 高级工程师模拟面试问答》
《Android 高级工程师面试终极速背版》
《Kotlin MVVM 实战入门:从分层到状态闭环》