Android 高级工程师面试参考答案:Kotlin MVVM 高频题、追问与项目表达

这篇适合怎么用

本文聚焦 Kotlin 技术栈下的 MVVM :面试官常问的落点、追问方向,以及你可以直接替换项目名词套用的句式。你不需要先读其他系列文章,也能直接使用本文的答题框架。

161721767

如果想补代码闭环,可看上一篇 《Kotlin MVVM 实战入门:从分层到状态闭环》。如果你已经会写,本文重点是把它讲成「复杂度怎么被收敛」,而不是背框架名。

适合读者 :有 Kotlin 基础、写过几个 Android 页面、想系统化准备 MVVM 面试表达的工程师。不要求精通协程和 Flow,但建议至少知道 ViewModel、协程作用域和状态流的基本概念。

1. 你怎么理解 Kotlin 项目里的 MVVM?

先看一张最小流向图:

MVVM 的重点不是三个文件夹,而是流向清晰:View 发用户意图,ViewModel 组织异步并更新状态,Repository 处理远程/本地数据,最后由 StateFlow/UiState 驱动 UI 渲染。

参考答法

MVVM 在我这儿首先是状态与职责 问题:View 只负责展示和把用户操作交给 ViewModelViewModel 在生命周期内保留页面状态、用协程组织异步;Model 这一侧我通常拆成 Repository 或再下一层的数据源,避免 Activity 里堆接口回调。Kotlin 带来的好处是协程 + Flow 让异步链路更像顺序代码,但模式本身不自动解决复杂度,关键还是状态是否收口、事件是否分离、边界是否清楚。」

面试官在听什么

  • 你有没有把 MVVM 说成「三个文件夹名字」
  • 你是否知道异步与生命周期在 Kotlin 里怎么对齐

怎么说才加分

  • 主动提「状态 vs 一次性事件」分离。
  • 用一句项目话收尾:「我们当时主要解决的是状态散在 FragmentAdapter 里难排查的问题。」
  • 不要把亮点说成「用了 MVVM」,要说「原来什么复杂度被它收敛了」。

2. ViewModel 为什么能处理配置变更?你在项目里怎么用它?

参考答法

「配置变更时 Activity/Fragment 会重建,但 ViewModel 是按作用域缓存的,所以适合保存与当前页面强相关、又与具体 View 实例无关 的状态。我会让 ViewModel 持有 StateFlow 表达的 UiState,页面只订阅;作用域销毁时 viewModelScope 会取消协程,避免页面没了请求还在跑。」

追问怎么接

  • 为什么不要持有 Activity
    ViewModel 可能比某次 Activity 实例活得更久,持有引用容易泄漏,也让测试和职责划分变糊。」
  • 为什么旋转后还能拿到同一个 ViewModel?
    「不是 ViewModel 自己不会销毁,而是配置变更时 ViewModelStore 被保留下来;新的 Activity/Fragment 会通过同一个 ViewModelStoreOwner 重新拿到原来的 ViewModel 实例。」
  • SavedStateHandle 用来干什么?
    「少量要进进程恢复栈的状态,比如选中 tab、搜索词;大对象仍走自己的存储或重新拉取。」
  • 什么不该放进 ViewModel
    「不放 ViewActivity Context、大缓存、长生命周期单例,也不直接创建网络客户端。ViewModel 是页面状态协调者,不是万能仓库。」

直接套用句式

「我把 ViewModel 当页面状态协调者:跨旋转保留该保留的;该进系统恢复链路的轻量状态用 SavedStateHandle,该持久化的数据交给下层仓库。」


3. 为什么很多 Kotlin 项目用 StateFlow 承载页面状态?

参考答法

StateFlow 是热流,始终持有当前值 ,适合表达「此刻页面长什么样」;普通 Flow 默认是冷流,每次 collect 都会重新执行上游逻辑,更适合描述一次数据处理管道。和 LiveData 比,StateFlow 与协程生态统一、组合操作更直观。StateFlow 本身不感知生命周期,生命周期管理由 collect 端负责,比如用 repeatOnLifecyclecollectAsStateWithLifecycle 把收集范围绑到页面可见生命周期。」

追问怎么接

  • 单次事件为什么容易踩坑?
    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 层要用 repeatOnLifecyclecollectAsStateWithLifecycle 来接入生命周期。」

4. 协程在 MVVM 里扮演什么角色?viewModelScopelifecycleScope 怎么分工?

参考答法

「业务异步我默认放在 ViewModel 里用 viewModelScope 启动,这样与页面作用域一致 ,销毁即取消。lifecycleScope 更适合紧贴 UI 的一次性动画、延迟与页面级副作用;若逻辑会跨配置变更,仍应沉到 ViewModel。」

追问怎么接

  • 取消不生效?
    「要看有没有挂起点 、是否在阻塞调用上;结构化并发是否把子任务挂在同一 coroutineContext。」
  • 怎么确认取消真的生效?
    「调试时可以在协程 finally 里打日志,看页面退出或 ViewModel 清理后是否执行;测试里可以用 runTest + TestDispatcher 控制调度,断言任务取消后的状态变化,而不是只靠肉眼看请求有没有返回。」
  • 为什么不全局 launch?
    「难追踪、难取消、难测,线上问题不好归因。」
  • 多个子协程里一个失败,其他会怎样?
    「结构化并发默认会把失败向上传播,一个子协程异常可能导致同级任务被取消。需要隔离失败时,可以用 supervisorScopeSupervisorJob,但要明确异常由谁处理,不能只是为了"不崩"而吞错误。」
  • 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 里编排会变重。
  • 业务规则需要复用,比如权限判断、风控逻辑、排序过滤、价格计算。
  • 同一动作会被多个入口触发,希望集中维护规则和测试。
  • 如果只是简单转发 RepositoryUseCase 往往只是增加层级。

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 解决依赖创建、作用域管理和测试替换。」

面试官在听什么

  • 你是否知道每个组件解决的边界问题,而不是只会列名字。
  • 你是否能把 JetpackMVVM 的职责接起来:状态归 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 里不要用 thisLifecycleOwner,要用 viewLifecycleOwner,避免 View 销毁后还继续更新旧视图。」
  • 为什么 LiveData 不更新?
    「常见原因有:原地改了同一个对象但没有重新 setValue;在后台线程调用了 setValuepostValue 连续多次只观察到最后一次;观察者还没进入活跃生命周期;或者用了错误的 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 次。」
  • 「线上页面首屏可交互时间 P95A ms 降到 B ms。」
    • 这里的 P95 可以理解为:按耗时从快到慢排序后,95% 用户都不超过这个耗时,比平均值更能反映大多数偏慢用户的体验。
  • 「相关页面 ANR / 崩溃率在两周内从 A% 降到 B%。」

如果被追问"数据怎么来的",可以补这一句:

「这些数字来自 Crashlytics / 线上埋点的改造前后同口径对比(通常看两周窗口),MVVM 本身不是魔法,关键是把状态收口到 UiState 后,散落在多个 LiveData/View 的并发改写问题更容易被消除和验证。」

注意:没有真实数据就不要硬编数字。可以说「当时目标是把 P95 降到某个范围」,或者「内部复盘时观察到某类问题明显减少,但没有做严格归因统计」。

常见反模式(面试主动提 = 加分):

  • ViewModel 持有 ActivityView 或短生命周期 Context
  • View 直接调用 Repository,把页面逻辑和数据策略搅在一起。
  • 把一次性导航、Toast 当成稳定状态保存,导致重建后重复消费。
  • 多协程直接读写 StateFlow.value,不用 update,产生状态覆盖。
  • try/catchrunCatching 吞掉 CancellationException,把取消当失败态展示。

12. 面试官最喜欢听到的关键词

回答 MVVM 时,如果能自然带出这些词,通常比单纯介绍 ViewModelRepositoryStateFlow 更能体现工程经验:

  • 状态收口
  • 状态与事件分离
  • 单向数据流
  • 生命周期感知
  • 结构化并发
  • 可替换依赖
  • 职责边界
  • 配置变更恢复
  • 状态驱动 UI
  • 可测试性

13. 面试现场收口模板(可直接背骨架)

「我们 Kotlin 化之后,页面侧用 ViewModel + StateFlow 收口展示状态,异步走协程和 Repository;一次性动作用 SharedFlow / 事件流和状态分开,避免旋转重放。生命周期收集用 repeatOnLifecyclecollectAsStateWithLifecycle,避免不可见页面继续消费 UI 更新。收益是迭代和排查更顺,代价是前期要把 UiState 建模想清楚,不能边写边堆字段。」


14. 本篇高频追问速查

方向 高频追问 面试官在考什么
生命周期 repeatOnLifecycle 解决什么问题 生命周期感知与资源浪费
ViewModel 为什么旋转后状态不丢 ViewModelStore 与作用域
LiveDataStateFlowSharedFlow 分工 状态建模与事件语义
协程 取消、阻塞、异常隔离、重复请求策略 结构化并发与稳定性
Jetpack 常用组件、PagingDataStore 各自解决什么问题 组件边界与场景选择
边界 Repository vs DataSourceUseCase 什么时候值得加一层 分层职责与复杂度控制
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 实战入门:从分层到状态闭环》

相关推荐
Oo_行者_oO2 小时前
基于 SpEL Bean 注入的优雅权限控制方案
面试
唔662 小时前
在 Flutter 混合开发中,Android 原生层通知 Dart 界面更新状态
android·flutter
故渊at2 小时前
系列一:架构思想进阶 | 第1篇 Android 架构演进实录:从 MVC 的“万能类”到 MVVM 的数据驱动
android·架构·mvc
流星白龙3 小时前
【MySQL高阶】22.双写缓冲区,重做日志
android·mysql·adb
世人万千丶3 小时前
鸿蒙PC问题解决:窗口配置错误修复指南
android·学习·华为·开源·harmonyos·鸿蒙·鸿蒙系统
Raink老师3 小时前
【AI面试临阵磨枪-96】A2A 通信模式:请求响应、发布订阅、事件广播、消息队列?
面试·职场和发展
8Qi83 小时前
LeetCode 474:一和零(Ones and Zeroes)—— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包
西安邮电大学3 小时前
分布式锁三种实现
java·redis·后端·其他·面试