ViewModel 与状态恢复:SavedStateHandle 与 Factory
文章目录第 1 章 配置变更与进程死亡为什么需要 ViewModel原理补充踩坑动手练第 2 章 StateFlow表现层状态的现代默认原理补充踩坑动手练第 3 章 viewModelScope结构化并发与取消踩坑动手练第 4 章 SavedStateHandle进程恢复与导航参数原理补充踩坑动手练第 5 章 ViewModelFactory构造注入与测试替身踩坑动手练第 6 章 反模式与 LiveData 迁移边界动手练面试速查 · 追问链追问链 #1ViewModel 为什么能在旋转后存活 追问链 #2StateFlow 和 LiveData 新代码怎么选 追问链 #3SavedStateHandle 存什么不存什么 ⭐追问链 #4viewModelScope 取消语义 ⭐追问链 #5ViewModelFactory 测试怎么做 完整链路一句通相关推荐第 1 章 配置变更与进程死亡为什么需要 ViewModelJava 时代常见写法筛选条件、滚动位置、表单草稿写在Activity字段里。旋转屏触发onDestroy/onCreate字段归零低内存杀进程后更是全部丢失。onSaveInstanceState只能存轻量Bundle复杂对象要自己序列化且容易和 UI 逻辑缠在一起。ViewModel 的设计目标很直接在配置变更时保留 UI 相关状态生命周期长于Activity/Fragment短于应用进程。它不知道 View只暴露状态与意图Intent/Action符合 MVVM 单向数据流UDF。classOrderListViewModel(privatevalrepository:OrderRepository,):ViewModel(){privateval_uiStateMutableStateFlowOrderListUiState(OrderListUiState.Loading)valuiState:StateFlowOrderListUiState_uiState.asStateFlow()funrefresh(){viewModelScope.launch{_uiState.valueOrderListUiState.Loading runCatching{repository.getOrders()}.onSuccess{orders-_uiState.valueif(orders.isEmpty()){OrderListUiState.Empty}else{OrderListUiState.Success(orders)}}.onFailure{e-_uiState.valueOrderListUiState.Error(e.message?:加载失败)}}}}ViewModel由ViewModelStore持有同一ViewModelStoreOwner通常是NavBackStackEntry或Activity在配置变更后拿到同一实例。进程被系统回收时 ViewModel 也会销毁——那是SavedStateHandle的战场第 4 章。原理补充ComponentActivity在onRetainCustomNonConfigurationInstance链路之外通过ViewModelStoreOwner把 store 存进NonConfigurationInstances。Fragment 的viewModels()委托把 store 绑定到 Fragment 自身或 Nav Graph 的 back stack entry决定「同屏多 Fragment 是否共享状态」。踩坑把Activity/Context存进 ViewModel → 泄漏需要Application用AndroidViewModel或 DI 注入仓库。在 ViewModel 里直接持有View引用 → 同样泄漏。用GlobalScope替代viewModelScope→ 配置变更后协程不受控可能更新已销毁 UI。动手练A用一句话说明 ViewModel 比onSaveInstanceState更适合保存「订单筛选条件」的原因。D在所在工程搜索: ViewModel()确认是否有Context字段或GlobalScope。第 2 章 StateFlow表现层状态的现代默认新工程 UI 状态首选StateFlow而不是把 LiveData 当作默认方案。StateFlow是热流始终有一个当前值collect时立刻收到最新状态适合「屏幕当前长什么样」。约定私有可变 公开只读。privateval_uiStateMutableStateFlow(ExploreUiState())valuiState:StateFlowExploreUiState_uiState.asStateFlow()funonQueryChanged(query:String){_uiState.update{it.copy(queryquery)}}update原子地基于旧值生成新值避免多线程下读-改-写竞态。UI 层用data class或sealed interface表达状态机sealedinterfaceOrderListUiState{dataobjectLoading:OrderListUiStatedataobjectEmpty:OrderListUiStatedataclassSuccess(valorders:ListOrder):OrderListUiStatedataclassError(valmessage:String):OrderListUiState}Compose 侧collectAsStateWithLifecycle()View 体系用repeatOnLifecycle(STARTED)收集避免STOPPED后仍收事件浪费电量。viewLifecycleOwner.lifecycleScope.launch{viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){viewModel.uiState.collect{state-render(state)}}}一次性事件Toast、导航、Snackbar不要塞进StateFlow反复重放——用ChannelreceiveAsFlow()或显式SharedFlow(replay0)与状态流分离。原理补充StateFlow用 CAS 更新stateFlow字段collect通过AbstractSharedFlow管理订阅。value赋值等价于compareAndSet始终有值故适合 UI State。对比 LiveData无生命周期感知内建需repeatOnLifecycle但可与 Kotlin Flow 算子无缝组合。踩坑对外暴露MutableStateFlow→ 调用方可直接改状态破坏 UDF。把导航事件放在StateFlow→ 旋转屏后重复导航。在init里launch却不处理CancellationException→ 正常取消被当成错误打日志。动手练A写出private/val封装的StateFlow三行模板。B起点下面暴露了可变流改为只读classBadVm:ViewModel(){valuiStateMutableStateFlow(UiState())}第 3 章 viewModelScope结构化并发与取消viewModelScope是挂在 ViewModel 上的CoroutineScopeDispatchers.Main.immediate为默认并在ViewModel.onCleared()时cancel()。所有异步工作应落在这个作用域内配置变更不会取消正在进行的网络请求因为 ViewModel 还在只有 ViewModel 真正销毁时才统一取消。funloadDetail(id:String){viewModelScope.launch{_uiState.update{it.copy(loadingtrue)}try{valdetailrepository.getDetail(id)_uiState.update{it.copy(loadingfalse,detaildetail)}}catch(e:CancellationException){throwe// 必须再抛勿吞掉}catch(e:Exception){_uiState.update{it.copy(loadingfalse,errore.message)}}}}多个并行任务用asyncawait仍在同一viewModelScopeviewModelScope.launch{valheaderasync{repository.getHeader()}valitemsasync{repository.getItems()}_uiState.valueUiState(header.await(),items.await())}需要切换线程时在launch内withContext(Dispatchers.IO)不要把viewModelScope默认 Main 当成「只能主线程」。踩坑viewModelScope.launch(Dispatchers.IO) { _uiState.value ... }直接改 StateFlow 可以但复杂 UI 更新仍建议在 Main 收敛。子协程用GlobalScope.launch拉活后台任务 → ViewModel 清了还在跑。忘记CancellationException重抛 → 协程取消语义被破坏。动手练C为refresh()加withContext(Dispatchers.IO)调 Repository5 行骨架。AonCleared时viewModelScope会发生什么一句话。第 4 章 SavedStateHandle进程恢复与导航参数配置变更 ViewModel 能扛进程死亡后 ViewModel 实例也没了。要把小量可序列化状态写进SavedStateHandle它底层对接SavedStateRegistry与Bundle规则一致基本类型、String、Parcelable、Serializable。导航参数推荐走 SavedStateHandle而不是手写arguments?.getStringHiltViewModelclassDetailViewModelInjectconstructor(savedStateHandle:SavedStateHandle,privatevalrepository:OrderRepository,):ViewModel(){privatevalorderId:StringsavedStateHandle.getString(orderId)?:error(orderId required)privateval_uiStateMutableStateFlowDetailUiState(DetailUiState.Loading)valuiState_uiState.asStateFlow()init{load()}privatefunload(){viewModelScope.launch{valdetailrepository.getDetail(orderId)_uiState.valueDetailUiState.Success(detail)}}}用户输入的草稿可双向绑定到 handlevardraft:Stringget()savedStateHandle.getString(draft).orEmpty()set(value)savedStateHandle.set(draft,value)可选枚举解析模式与作用域函数篇呼应privatevalcategory:Category?savedStateHandle.getString(category)?.let{name-runCatching{Category.valueOf(name)}.getOrNull()}原理补充SavedStateHandle由AbstractSavedStateViewModelFactory或 Hilt 的HiltViewModelFactory注入。set的键会进入SavedStateProvider进程重建后get能读回。大数据列表、图片应走 Room/磁盘不要塞进 Bundle。踩坑把整页ListOrder塞进 SavedStateHandle →TransactionTooLargeException。导航参数名与 XML/graph 不一致 → 运行时null。只依赖 ViewModel 字段不写入 handle → 进程杀后草稿丢失。动手练B起点补全从SavedStateHandle读取filter并触发refresh(filter)的init逻辑≤8 行。D检查所在工程导航图参数是否在 ViewModel 中通过 handle 读取。第 5 章 ViewModelFactory构造注入与测试替身ViewModel需要Repository、SavedStateHandle、路由参数时不能无参构造。Hilt 新工程用HiltViewModelInject constructor为默认无 Hilt 或单元测试时要写Factory。classOrderListViewModelFactory(privatevalrepository:OrderRepository,):ViewModelProvider.Factory{Suppress(UNCHECKED_CAST)overridefunT:ViewModelcreate(modelClass:ClassT):T{if(modelClass.isAssignableFrom(OrderListViewModel::class.java)){returnOrderListViewModel(repository)asT}throwIllegalArgumentException(Unknown ViewModel:${modelClass.name})}}Fragment 中privatevalviewModel:OrderListViewModelbyviewModels{OrderListViewModelFactory(repository)}带 SavedState 的工厂用AbstractSavedStateViewModelFactoryclassDetailViewModelFactory(owner:SavedStateRegistryOwner,privatevalrepository:OrderRepository,privatevaldefaultArgs:Bundle?null,):AbstractSavedStateViewModelFactory(owner,defaultArgs){Suppress(UNCHECKED_CAST)overridefunT:ViewModelcreate(key:String,modelClass:ClassT,handle:SavedStateHandle,):TDetailViewModel(handle,repository)asT}单元测试用ViewModelProvider.NewInstanceFactory或直接OrderListViewModel(fakeRepo)配合runTestTurbine断言uiState发射序列。踩坑每个 ViewModel 在 Activity 里手写ViewModelProvider(this).get→ 应统一by viewModels()委托。Factory 里new具体 Repository → 测试无法换 Fake通过构造参数注入接口。混淆后modelClass判断失败 → 用isAssignableFrom或 Hilt 代码生成。动手练C为FakeOrderRepository写测试调用refresh()后uiState应为SuccessrunTest Turbine 骨架。Aby viewModels()与by activityViewModels()作用域差异一句话。第 6 章 反模式与 LiveData 迁移边界反模式问题改法Activity 存业务状态旋转丢失迁入 ViewModel StateFlowMutableLiveData新功能与 Flow 生态割裂新代码 StateFlow事件用 StateFlow重复消费Channel / SharedFlowViewModel 调findViewById层次坍塌UI 层 collect 渲染超大列表状态在内存OOM / 恢复失败Room PagingLiveData遗留工程维护时可与 StateFlow 互操作liveData.asFlow()、stateFlow.asLiveData()但新模块默认 StateFlow。从 Java 迁移时先把「异步 状态」从 Activity 抽到 ViewModel再把LiveData换成StateFlow通常第二步即可。动手练A列出「应放 ViewModel」vs「应放 SavedStateHandle」各两项。D 验收清单自选一屏旋转 开发者选项「不保留活动」各测一次筛选/草稿是否按设计恢复。面试速查 · 追问链追问链 #1ViewModel 为什么能在旋转后存活 标准回答≤200 字ViewModel实例由ViewModelStore持有store 在配置变更时随NonConfigurationInstances保留。新Activity/Fragment通过同一ViewModelStoreOwner取回同一对象。生命周期到ViewModel.onCleared()不是到onDestroy()。进程死亡则 store 也没了需SavedStateHandle。追问 1Fragment 的by viewModels()和by activityViewModels()答前者作用域在本 Fragment后者共享 Activity 级 store适合多 Fragment 共享同一业务状态。追问 2ViewModel 能替代onSaveInstanceState吗答配置变更可以进程杀不行。轻量可序列化状态用 SavedStateHandle大数据用本地库。追问链 #2StateFlow 和 LiveData 新代码怎么选 标准回答≤200 字新工程默认StateFlow与 Kotlin Flow、协程算子一致易于combine、flatMapLatest。需repeatOnLifecycle保安全收集。LiveData 仅遗留维护或asFlow()桥接。一次性事件不用 StateFlow防旋转重放。追问 1StateFlow和SharedFlow区别答StateFlow 必有当前值、适合状态SharedFlow 可无初始值、可配置 replay适合事件总线。追问 2为什么暴露asStateFlow()而不是MutableStateFlow答封装写权限强制 UDFUI 只读改状态只能走 ViewModel 方法。追问链 #3SavedStateHandle 存什么不存什么 ⭐标准回答≤200 字存导航参数、筛选 id、表单草稿等小而可序列化的值。不存大列表、Bitmap、整个 Domain 图。列表用 Room图片用路径或 id。底层 Bundle 有大小限制TransactionTooLargeException是常见事故。追问 1和rememberSaveableCompose关系答Compose 控件级轻量状态用rememberSaveable业务 ViewModel 状态仍走 SavedStateHandle ViewModel。追问 2Hilt 如何拿到 SavedStateHandle答HiltViewModel构造参数直接注入SavedStateHandle无需手写 Factory。追问链 #4viewModelScope 取消语义 ⭐标准回答≤200 字onCleared()触发viewModelScope.cancel()子协程收到CancellationException。配置变更不会clear ViewModel故进行中的请求可继续。须在 catch 中重抛CancellationException其余异常映射为 UiState 错误。追问 1旋转时网络请求会断吗答默认不断因 ViewModel 还在若希望随 UI 取消应用viewLifecycleOwner.lifecycleScope发请求较少用于可恢复数据。追问 2launch默认哪个 Dispatcher答viewModelScope默认Main.immediateIO 在块内withContext(Dispatchers.IO)。追问链 #5ViewModelFactory 测试怎么做 标准回答≤200 字构造注入FakeRepository直接ViewModel(fake)或自定义Factory。用runTest驱动协程turbine.testIn收集uiState断言发射序列。不依赖 Robolectric 亦可测纯逻辑。追问 1为什么不ViewModelProvider.NewInstanceFactory生产环境答无法注入依赖生产用 Hilt 或手写 Factory 传 Repository。完整链路一句通UI 发意图到 ViewModel→viewModelScope调 Repository→MutableStateFlow私有、asStateFlow公开→repeatOnLifecycle收集→导航/草稿进 SavedStateHandle→构造注入靠 Factory 或 Hilt。相关推荐Kotlin 语法与空安全Android 开发第一课Kotlin 作用域函数let/apply 工程选型

相关新闻

Neon autoscaling vm-builder 代码流程分析

Neon autoscaling vm-builder 代码流程分析

一、工具定位vm-builder 是一个 Go CLI 工具,核心功能:将任意 Docker 镜像转换为可在 QEMU VM 中运行的磁盘镜像(qcow2)。它把容器化应用"打包"成一个完整的虚拟机环境,使用 busybox init 作为初始化系统&am…

2026/10/28 12:01:25 阅读更多 →
Neon vm-monitor机制

Neon vm-monitor机制

核心逻辑1. 读的文件:/sys/fs/cgroup/neon-postgres/memory.stat2. 取的字段:active_anon inactive_anon(cgroup.rs:168)3. 不是瞬时值,而是最近 5 个样本(500ms)的平均值(cgroup.r…

2026/10/25 10:27:34 阅读更多 →
Neon autoscaler-agent

Neon autoscaler-agent

通过 HTTP 轮询 VM 内部的 Vector 进程(Prometheus 格式 metrics),具体链路:VM 内部 Vector (host_metrics 源, 每秒采集) →暴露 /metrics 端点 (默认 9100 端口) →autoscaler-agent 每 5 秒 HTTP GET 拉取 →用 prometheus exp…

2026/10/27 2:04:01 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/28 23:48:14 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/29 8:45:01 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/29 9:52:56 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/28 22:10:30 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/29 9:51:34 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/29 8:44:28 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 16:58:40 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/29 9:42:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 19:25:09 阅读更多 →