ViewModel:生命周期管理与数据持久化核心实战
在上一篇中,我们完成了 Jetpack 专栏的环境搭建与工程初始化,统一了 Kotlin + Gradle Kotlin DSL + Version Catalog 的现代开发规范。从本篇开始,我们将正式进入核心组件的深度实战。
屏幕旋转一下数据就全丢了?onSaveInstanceState 只能存点基本类型?如果你还在被这些问题困扰,那么 ViewModel 就是你要找的解药。本文将带你从原理到源码彻底吃透 ViewModel,亲手解决配置变更数据丢失的经典痛点,并掌握 ViewModelFactory 与 Fragment 间数据共享的标准写法,所有代码均采用地道 Kotlin 规范且经过编译验证。
一、ViewModel 诞生背景:为何我们需要它?
在 ViewModel 出现之前,Android 开发者面对配置变更(屏幕旋转、语言切换、分屏模式、字体缩放等)导致的数据丢失问题,主要有两种传统应对方式,但都存在明显缺陷。
1. 传统方案一:onSaveInstanceState + onRestoreInstanceState
这是系统提供的原生方案,但它的能力边界非常有限:
- 只能存储基本类型和可序列化的轻量对象,无法保存 List 集合、Bitmap、正在执行的网络请求与耗时任务
- 数据存取需要经历序列化 / 反序列化过程,存在性能开销
- 样板代码多,复杂状态管理起来非常繁琐
它只能作为页面状态的兜底备份,无法承担核心业务数据的持久化。
2. 传统方案二:静态变量 / 单例手动暂存
部分开发者会用静态变量或全局单例来暂存页面数据,但弊端同样明显:
- 无法感知界面生命周期,数据清理完全依赖手动管理,极易造成内存泄漏
- 多页面复用容易出现数据错乱,状态管理混乱
- 不符合架构解耦的设计思想,代码可维护性差
3. ViewModel 的解决方案
ViewModel 被设计用来以生命周期感知的方式存储和管理 UI 相关数据。它的核心价值在于:让数据脱离 Activity/Fragment 的生命周期而存在,当配置变更发生时,ViewModel 实例不会被销毁,数据自然也就不会丢失。
同时 ViewModel 配合官方架构规范,可以将业务逻辑从界面代码中剥离,从根源上降低内存泄漏风险,提升代码可维护性。
二、ViewModel 生命周期详解
理解 ViewModel 的生命周期,是正确使用它的第一步,也是区分它与普通数据类的核心。
1. 生命周期范围
ViewModel 的生命周期与它绑定的 ViewModelStoreOwner(通常是 Activity 或 Fragment)的生命周期并不一致,完整分为三个阶段:
| 阶段 | 触发时机 | 对应行为 |
|---|---|---|
| 创建 | 首次通过 ViewModelProvider 请求 ViewModel 时 | 执行构造函数,初始化内部数据 |
| 存活 | 从首次请求直到绑定的 Activity/Fragment 彻底销毁 | 实例保持不变,持续持有数据,配置变更不会中断 |
| 销毁 | Activity 被 finish / Fragment 被 detach 时 | 回调 onCleared() 方法,释放内部资源 |
核心结论:ViewModel 不会在屏幕旋转时被销毁。系统在配置变更时只是重建了 Activity 实例,但 ViewModel 的存储容器会被保留,因此数据可以无缝延续。
2. 需要警惕的特殊情况
ViewModel 并非万能。如果 App 因内存不足被系统在后台 Kill 掉,ViewModel 实例也会随之被清除,数据会丢失。
这种场景下,需要结合 SavedStateHandle 来保存关键状态,实现「进程被杀也能恢复」的兜底能力,我们会在后续进阶篇中详细讲解。
三、源码级原理剖析:ViewModel 是如何做到数据不丢失的?
光知道「怎么用」还不够,搞懂「为什么」才能让你遇到问题时游刃有余。下面我们从源码层面拆解 ViewModel 的核心运作机制。
1. 核心三剑客:ViewModelStore、ViewModelStoreOwner、ViewModelProvider
ViewModel 的整个运作体系由三个核心角色协作完成,各司其职。
① ViewModelStore ------ 存储容器
ViewModelStore 是 ViewModel 的存储容器,我们看它的核心源码(精简后):
java
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
mMap.put(key, viewModel);
}
final ViewModel get(String key) {
return mMap.get(key);
}
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear(); // 内部回调 onCleared()
}
mMap.clear();
}
}
本质上就是一个 HashMap,以字符串为 key、ViewModel 实例为 value,负责存储和管理所有关联的 ViewModel 对象。
② ViewModelStoreOwner ------ 容器的拥有者
这是一个接口,ComponentActivity 和 Fragment 都默认实现了它。 每个 Activity / Fragment 都对应一个 Owner,持有一个独立的 ViewModelStore 配置变更时,新的界面实例会继承旧实例的 Owner 身份,关联同一个 ViewModelStore
③ ViewModelProvider ------ 创建与获取的入口
它是 ViewModel 的统一获取工具,负责「有则返回、无则创建」。我们日常写的:
kotlin
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
内部逻辑就是:接收 Owner → 拿到 Store → 查找实例 → 不存在则用 Factory 创建。
2. 获取 ViewModel 的完整流程
当你调用 ViewModelProvider(this).get(...) 时,背后完整执行了 5 步:
- 构造 Provider:传入的 Activity/Fragment 作为 Owner,Provider 从中获取 ViewModelStore 和默认工厂
- 查找缓存:以 ViewModel 类名为 key,从 ViewModelStore 的 HashMap 中查找已有实例
- 命中返回:如果找到实例,直接返回,不会重复创建
- 未命中创建:如果没找到,使用 Factory 通过反射创建新的 ViewModel 实例
- 存入缓存:将新实例存入 ViewModelStore,供后续复用
3. 为什么屏幕旋转后数据还在?
核心秘密就在 ViewModelStore 的归属上:
- ViewModelStore 不属于某个具体的 Activity 实例,而是属于 ViewModelStoreOwner 这个「逻辑身份」
- 屏幕旋转时,旧 Activity 被销毁,新 Activity 被创建,但系统会将新 Activity 与同一个 Owner 关联
- 因此新 Activity 通过 Provider 获取 ViewModel 时,从同一个 Store 里拿到的还是原来的实例,数据自然完整保留
4. onCleared ():资源清理的最后防线
当绑定的 Activity/Fragment 真正销毁时,ViewModelStore 会执行 clear() 方法,遍历所有 ViewModel 并回调 onCleared()。 这是防止内存泄漏的最后一道防线,我们可以在这里:
- 取消未完成的网络请求
- 释放大对象引用
- 取消监听器与订阅
注意:viewModelScope 协程作用域会在 onCleared() 时自动取消,无需手动处理。
四、前置准备:统一开发规范
按照专栏约定,我们全程使用 ViewBinding + Version Catalog + 地道 Kotlin 的开发规范,先确认基础配置。
1. 开启 ViewBinding
在 app/build.gradle.kts 中开启视图绑定:
kotlin
android {
namespace = "com.example.jetpacknote"
compileSdk = 34
buildFeatures {
viewBinding = true
}
// ... 其余配置保持不变
}
2. 补充 Fragment KTX 依赖
activityViewModels() 等扩展来自 fragment-ktx,已在 Version Catalog 中统一配置:
libs.versions.toml 中已包含:
toml
[versions]
fragment = "1.6.2"
[libraries]
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" }
app/build.gradle.kts 中引入:
kotlin
dependencies {
implementation(libs.androidx.fragment.ktx)
}
五、基础实战:计数器 + 屏幕旋转验证
理论讲完,我们动手编码。本节实现经典计数器案例,验证屏幕旋转时数据不丢失。
1. 创建计数器 ViewModel
在 viewmodel 包下创建 CounterViewModel.kt:
kotlin
package com.example.jetpacknote.viewmodel
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
// 对外只读、对内可写,遵循数据封装原则
var count = 0
private set
fun increment() {
count++
}
fun decrement() {
count--
}
fun reset() {
count = 0
}
}
2. 编写布局文件
修改 activity_main.xml,采用垂直线性布局,包含显示文本与三个操作按钮:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<TextView
android:id="@+id/tvCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="64sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="32dp">
<Button
android:id="@+id/btnDecrement"
android:layout_width="80dp"
android:layout_height="56dp"
android:text="-" />
<Button
android:id="@+id/btnReset"
android:layout_width="80dp"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:text="重置" />
<Button
android:id="@+id/btnIncrement"
android:layout_width="80dp"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:text="+" />
</LinearLayout>
</LinearLayout>
3. Activity 中使用 ViewModel + ViewBinding
修改 MainActivity.kt,采用 KTX 属性委托 + ViewBinding 标准写法:
kotlin
package com.example.jetpacknote
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.viewModels
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.viewmodel.CounterViewModel
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initClickListeners()
updateCounterDisplay()
}
private fun initClickListeners() = with(binding) {
btnIncrement.setOnClickListener {
viewModel.increment()
updateCounterDisplay()
}
btnDecrement.setOnClickListener {
viewModel.decrement()
updateCounterDisplay()
}
btnReset.setOnClickListener {
viewModel.reset()
updateCounterDisplay()
}
}
private fun updateCounterDisplay() {
binding.tvCounter.text = viewModel.count.toString()
}
}
4. 验证屏幕旋转数据保留
运行 App,点击「+」将计数增加到任意值(比如 5),然后旋转屏幕:
- ✅ 预期结果:数值保持不变,没有重置为 0
- ❌ 异常排查:如果数据丢失,检查是否直接 CounterViewModel() 手动创建实例,必须通过 viewModels() 委托获取
六、进阶实战:带参 ViewModel 与 Factory 封装
实际项目中,ViewModel 经常需要接收外部参数,比如 Intent 传入的 ID、Repository 依赖、初始值配置。默认工厂只能创建无参实例,带参场景必须自定义 ViewModelProvider.Factory。
1. 为什么需要自定义 Factory
ViewModel 的实例由 Provider 通过反射创建,默认工厂只会调用无参构造函数。如果构造函数带参数,直接使用 by viewModels() 会抛出 NoSuchMethodException 运行时异常。 自定义 Factory 的作用就是:接管 ViewModel 的创建逻辑,手动传入构造参数。
2. 自定义 Factory 标准实现
步骤 1:创建带参 ViewModel
kotlin
package com.example.jetpacknote.viewmodel
import androidx.lifecycle.ViewModel
class ParamCounterViewModel(
private val initialCount: Int
) : ViewModel() {
var count = initialCount
private set
fun increment() { count++ }
fun decrement() { count-- }
fun reset() { count = initialCount }
}
步骤 2:自定义 ViewModelFactory
kotlin
package com.example.jetpacknote.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ParamCounterViewModelFactory(
private val initialCount: Int
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = when {
modelClass.isAssignableFrom(ParamCounterViewModel::class.java) ->
ParamCounterViewModel(initialCount) as T
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
}
步骤 3:Activity 中使用
kotlin
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
// 传入工厂,指定初始值为 10
private val viewModel: ParamCounterViewModel by viewModels {
ParamCounterViewModelFactory(10)
}
// ... 其余逻辑不变
}
3. 进阶提示:CreationExtras 新写法
从 lifecycle-viewmodel 2.5.0 开始,Google 推荐使用 CreationExtras 方式传递参数,类型更安全、写法更简洁。 考虑到入门学习成本,本专栏前期以 Factory 标准写法为主,后续进阶篇会详细讲解 CreationExtras 的企业级用法。
七、场景实战:Fragment 间共享数据
多个 Fragment 共享数据是高频开发需求。传统做法通过 Activity 中转、接口回调层层传递,代码臃肿且难以维护。ViewModel 提供了更优雅的方案:同一 Activity 下的多个 Fragment 共享同一个 Activity 级 ViewModel 实例。
1. 两种作用域委托的区别
| 委托方式 | 作用域 | 适用场景 |
|---|---|---|
| by viewModels() | 当前 Fragment 自身 | 管理 Fragment 私有状态 |
| by activityViewModels() | 所属 Activity | 同一 Activity 下多 Fragment 共享数据 |
⚠️ 注意:如果 ViewModel 是带参构造,不能直接用 by activityViewModels(),否则默认无参工厂无法创建实例,会导致崩溃。下文采用全版本兼容的显式传工厂写法,可直接运行。
2. 完整实战实现
我们实现两个 Fragment:SenderFragment 发送消息,ReceiverFragment 接收并展示,通过共享 ViewModel 通信。
步骤 1:创建共享 ViewModel
kotlin
package com.example.jetpacknote.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SharedDataViewModel : ViewModel() {
// 私有可变,外部只暴露不可变 LiveData,保证数据流向唯一
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun sendMessage(content: String) {
_message.value = content
}
fun clearMessage() {
_message.value = null
}
}
步骤 2:Activity 暴露工厂实例
Activity 作为宿主,统一提供工厂实例,保证所有 Fragment 使用同一个 ViewModel:
kotlin
package com.example.jetpacknote
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.viewModels
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel
import com.example.jetpacknote.viewmodel.SharedDataViewModelFactory
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
// 公开工厂,供子 Fragment 复用
val sharedViewModelFactory = SharedDataViewModelFactory()
private val sharedViewModel: SharedDataViewModel by viewModels { sharedViewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
说明:本例 ViewModel 无参,工厂仅做演示;带参场景下,所有 Fragment 都会复用 Activity 中的同一个工厂实例,确保数据一致。
步骤 3:SenderFragment 发送端
kotlin
package com.example.jetpacknote.fragment
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.example.jetpacknote.MainActivity
import com.example.jetpacknote.R
import com.example.jetpacknote.databinding.FragmentSenderBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel
class SenderFragment : Fragment(R.layout.fragment_sender) {
private var _binding: FragmentSenderBinding? = null
private val binding get() = _binding!!
// 显式传入 Activity 中的工厂,保证实例唯一
private val sharedViewModel: SharedDataViewModel by activityViewModels {
(requireActivity() as MainActivity).sharedViewModelFactory
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSenderBinding.bind(view)
binding.btnSend.setOnClickListener {
val content = binding.etMessage.text.toString().trim()
if (content.isNotEmpty()) {
sharedViewModel.sendMessage(content)
binding.etMessage.text?.clear()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
步骤 4:ReceiverFragment 接收端
kotlin
package com.example.jetpacknote.fragment
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.example.jetpacknote.MainActivity
import com.example.jetpacknote.R
import com.example.jetpacknote.databinding.FragmentReceiverBinding
import com.example.jetpacknote.viewmodel.SharedDataViewModel
class ReceiverFragment : Fragment(R.layout.fragment_receiver) {
private var _binding: FragmentReceiverBinding? = null
private val binding get() = _binding!!
private val sharedViewModel: SharedDataViewModel by activityViewModels {
(requireActivity() as MainActivity).sharedViewModelFactory
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentReceiverBinding.bind(view)
// 观察数据变化,自动更新 UI
sharedViewModel.message.observe(viewLifecycleOwner) { message ->
binding.tvReceivedMessage.text = message ?: "暂无消息"
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
步骤 5:布局引入两个 Fragment
在 activity_main.xml 中通过 FragmentContainerView 加载两个 Fragment 即可,运行后发送消息,接收端会自动更新,旋转屏幕数据不丢失。
八、AndroidViewModel:需要 Context 时的正确选择
很多开发者会疑惑:ViewModel 不能持有 Context,但业务中确实需要用到 Context(比如读资源、取系统服务),该怎么办?答案是使用 AndroidViewModel。
1. ViewModel 与 AndroidViewModel 对比
| 特性 | ViewModel | AndroidViewModel |
|---|---|---|
| 构造参数 | 无参 | 接收 Application 实例 |
| 是否持有 Context | 否 | 持有 Application Context |
| 生命周期 | 跟随 Owner | 跟随 Owner,Application 无泄漏风险 |
| 适用场景 | 绝大多数业务场景 | 需要访问系统服务、全局资源时 |
2. 使用示例
kotlin
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
class MyAndroidViewModel(application: Application) : AndroidViewModel(application) {
private val prefs = application.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
fun saveData(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
}
3. 重要红线
- ✅ 允许:持有 Application Context(全局生命周期,不会造成泄漏)
- ❌ 禁止:持有 Activity Context、View、Fragment 引用(生命周期不一致,必然泄漏)
九、项目预集成:记事本 NoteViewModel 初版
结合专栏的记事本实战项目,我们提前搭建笔记列表 ViewModel 的骨架,为后续 Room 数据库接入做准备:
kotlin
package com.example.jetpacknote.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* 笔记列表 UI 状态封装
*/
data class NoteUiState(
val notes: List<String> = emptyList(), // 后续替换为 Note 实体类
val isLoading: Boolean = false,
val error: String? = null
)
class NoteViewModel : ViewModel() {
private val _uiState = MutableStateFlow(NoteUiState())
val uiState: StateFlow<NoteUiState> = _uiState.asStateFlow()
/**
* 加载笔记列表
* 目前模拟数据,后续对接 Room 数据库
*/
fun loadNotes() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
delay(1000) // 模拟网络/数据库耗时
_uiState.value = _uiState.value.copy(
isLoading = false,
notes = listOf("示例笔记 1", "示例笔记 2", "示例笔记 3")
)
}
}
/**
* 添加笔记
*/
fun addNote(content: String) {
val currentNotes = _uiState.value.notes.toMutableList()
currentNotes.add(content)
_uiState.value = _uiState.value.copy(notes = currentNotes)
}
}
这里我们先用 StateFlow 做状态管理,下一篇讲解 LiveData 后会同步提供 LiveData 版本的实现,并讲解两者的选型与迁移。
十、FAQ / 避坑指南
Q1:ViewModel 为何能跨越配置变更保留数据?
A:ViewModel 实例存储在 ViewModelStore 中,而 Store 归属于 ViewModelStoreOwner(逻辑身份),而非 Activity 实例本身。配置变更时,系统重建 Activity 实例,但新实例关联的是同一个 Owner,因此从 Store 中取出的仍是原来的 ViewModel,数据自然保留。
Q2:ViewModel 为什么禁止持有 Context / View 引用?
A:因为 ViewModel 的生命周期长于 Activity/Fragment。配置变更时 Activity 销毁,但 ViewModel 依然存活,如果持有界面引用,会阻止 Activity 被 GC 回收,造成内存泄漏。 必须用 Context 时请使用 AndroidViewModel,它持有的 Application Context 生命周期与 App 一致,无泄漏风险。
Q3:Fragment 共享带参 ViewModel 直接崩溃怎么办?
A:崩溃原因是 by activityViewModels() 默认使用系统无参工厂,无法创建带参实例。 正确做法:在 Activity 中定义公开的工厂实例,所有 Fragment 获取 ViewModel 时显式传入该工厂,确保大家使用同一个工厂、同一个实例。
Q4:onCleared () 什么时候调用?里面该做什么?
A:只有当 Activity 被 finish / Fragment 被 detach 时才会调用,屏幕旋转不会触发。 推荐在里面做:取消未完成的网络请求、释放大对象、清理监听器订阅。注意 viewModelScope 会自动取消,无需手动处理。
Q5:ViewModel 可以做全局跨页面数据共享吗?
A:不推荐。ViewModel 与 Owner 一一绑定,不同 Activity 对应不同的 Store,无法共享实例。全局数据共享应该使用 Repository 单例、全局数据仓库等方案。
写在最后
本篇我们从背景、生命周期、源码原理、基础实战、带参工厂、Fragment 共享、AndroidViewModel 到项目预集成,系统地吃透了 ViewModel 的核心知识与企业级用法。
但目前的实现中,数据变化后还需要手动刷新 UI,还没有真正实现「数据驱动 UI」。下一篇我们将正式进入 LiveData 的学习,探索响应式编程的核心优势,重点攻克粘性事件、数据倒灌等经典痛点,让 ViewModel 中的数据变化可以自动、安全地更新界面。
🌟 点赞 + 收藏 + 关注,第一时间收到更新推送 💬 评论区留言:你在使用 ViewModel 时遇到过哪些坑?或者对 LiveData 有什么疑问?欢迎留言交流!