背景
在前面的文章里,我们已经接触过 Activity、Fragment 和 RecyclerView。写到稍微复杂一点的页面时,一个常见问题会立刻出现:页面上的数据到底应该放在哪里?如果把用户输入、接口结果、列表状态都直接放进 Activity 或 Fragment,代码很快会变得臃肿,而且旋转屏幕、切换深色模式、语言变化等配置变化发生时,Activity 可能会被销毁重建,普通成员变量也会跟着丢失。
Jetpack ViewModel 和 LiveData 就是为了解决这类问题而设计的。ViewModel 负责保存和管理页面相关数据,LiveData 负责把可观察的数据安全地通知给界面层。它们的核心价值不是“少写几行代码”,而是让 UI 控制器更轻,让状态更稳定,让数据更新遵守生命周期。
简单说:Activity / Fragment 负责显示和响应用户操作,ViewModel 负责准备页面状态,LiveData 负责把状态变化通知给界面。
核心概念
ViewModel 是页面状态容器
ViewModel 的生命周期比 Activity / Fragment 的一次重建更长。当设备旋转导致 Activity 重建时,同一个 ViewModel 实例通常会被保留下来,因此适合保存页面级状态,比如当前选中的 Tab、列表筛选条件、表单草稿、接口加载结果等。
不过 ViewModel 不是万能缓存,也不是放所有对象的地方。它不应该长期持有 Activity、View、Context 等容易造成内存泄漏的对象。确实需要 Context 时,应优先使用 AndroidViewModel 的 Application Context,或者把依赖放到 Repository 层。
LiveData 是生命周期感知的数据
LiveData 是一个可观察数据持有者。它最重要的特点是生命周期感知:只有当 LifecycleOwner 处于 STARTED 或 RESUMED 等活跃状态时,观察者才会收到更新。页面销毁后,LiveData 会自动移除相关观察,避免很多手动反注册遗漏导致的问题。
LiveData 常见写法是:ViewModel 内部使用 MutableLiveData 修改数据,对外暴露不可变 LiveData。这样 UI 层只能观察,不能随意改状态,职责更清晰。
ViewModel 与 LiveData 的关系
ViewModel 管状态,LiveData 发通知。ViewModel 中可以有多个 LiveData,例如 loading、errorMessage、userList、selectedItem 等。UI 层订阅这些 LiveData,并在数据变化时刷新控件。
在现代 Android 开发中,StateFlow 也很常见,但 LiveData 依然适合入门 Jetpack 架构,尤其是在 XML + Fragment 的项目里,接入成本低,和生命周期结合自然。
代码实战(Kotlin)
下面做一个最小可运行的计数器页面:点击按钮让数字加一,旋转屏幕后数字不丢失。
先添加依赖。大多数新项目已经自带 lifecycle 依赖,如果没有,可以在模块级 build.gradle 中加入:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") // 提供 ViewModel Kotlin 扩展
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7") // 提供 LiveData Kotlin 扩展
implementation("androidx.activity:activity-ktx:1.9.3") // 提供 by viewModels() 委托
}
创建 ViewModel:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0) // 内部可修改,初始值为 0
val count: LiveData<Int> = _count // 对外只暴露只读 LiveData
fun increase() {
val current = _count.value ?: 0 // 读取当前值,避免 null 导致崩溃
_count.value = current + 1 // 主线程更新 LiveData
}
fun reset() {
_count.value = 0 // 将状态恢复到初始值
}
}
Activity 中观察数据并响应点击:
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
class CounterActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels() // 生命周期范围内获取同一个 ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val countText = findViewById<TextView>(R.id.countText)
val addButton = findViewById<Button>(R.id.addButton)
val resetButton = findViewById<Button>(R.id.resetButton)
viewModel.count.observe(this) { count -> // this 是 LifecycleOwner,页面销毁后自动停止观察
countText.text = "当前计数:$count" // 只在状态变化时刷新 UI
}
addButton.setOnClickListener {
viewModel.increase() // 用户操作交给 ViewModel 处理
}
resetButton.setOnClickListener {
viewModel.reset()
}
}
}
对应布局可以很简单:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/countText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前计数:0"
android:textSize="22sp" />
<Button
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加一" />
<Button
android:id="@+id/resetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重置" />
</LinearLayout>
运行后点击“加一”,数字会变化。此时旋转屏幕,Activity 会重新执行 onCreate(),但 CounterViewModel 仍然保留原来的 count 值,界面重新观察 LiveData 后会收到最新值,所以数字不会回到 0。
实际项目中,ViewModel 通常不会直接写网络请求或数据库细节,而是调用 Repository:
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableLiveData<UserUiState>()
val uiState: LiveData<UserUiState> = _uiState
fun loadUser(userId: String) {
_uiState.value = UserUiState.Loading // 先通知页面进入加载态
// 真实项目可结合协程 viewModelScope 调用 repository
}
}
这样 Activity 只关心“展示什么”,ViewModel 关心“状态是什么”,Repository 关心“数据从哪里来”。分层之后,页面会更容易维护和测试。
避坑指南
第一,不要在 ViewModel 中持有 Activity、Fragment 或 View。ViewModel 可能比页面实例活得更久,持有这些对象容易造成内存泄漏。需要资源字符串时,可以在 UI 层转换,或使用 Application Context。
第二,不要把 MutableLiveData 直接暴露给外部。推荐 _count 私有、count 公开的写法,避免任何类都能修改状态,导致数据流混乱。
第三,区分 setValue 和 postValue。在 Kotlin 属性写法中,_count.value = xxx 等价于主线程 setValue;如果在后台线程更新 LiveData,应使用 postValue,或者切回主线程再赋值。
第四,LiveData 不适合直接表示一次性事件。Toast、导航、弹窗这类事件如果用普通 LiveData,配置变化后可能被重复消费。可以使用 Event 包装、SharedFlow,或更明确的 UI 事件机制。
第五,不要让 ViewModel 变成“大杂烩”。ViewModel 是页面状态管理者,不是工具类集合。复杂业务应下沉到 UseCase、Repository 或独立组件里。
总结
ViewModel + LiveData 是 Android Jetpack 架构中非常经典的一组搭配。ViewModel 解决页面状态在配置变化中的保存问题,LiveData 解决数据变化通知与生命周期绑定问题。它们让 Activity 和 Fragment 从“什么都管”变成“只负责 UI”,这是从入门写法走向工程化写法的重要一步。
掌握这套模式后,再学习 Room、Retrofit、Navigation、WorkManager 或 MVVM 架构都会更顺。下一次你写页面时,可以先问自己三个问题:状态放在哪里?谁能修改状态?UI 如何感知状态变化?如果答案清晰,代码自然会更稳。