Jetpack ViewModel + LiveData

简介: 本文详解 Android Jetpack 中 ViewModel 与 LiveData 的核心用法:ViewModel 负责保存页面状态(如计数、列表数据),在屏幕旋转等配置变化中自动保留;LiveData 实现生命周期感知的数据观察,避免内存泄漏与空指针。通过计数器实战,演示分层设计——UI 层响应操作、ViewModel 管理状态、Repository 处理数据源,助你写出更稳定、可维护的代码。

背景

在前面的文章里,我们已经接触过 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 公开的写法,避免任何类都能修改状态,导致数据流混乱。

第三,区分 setValuepostValue。在 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 如何感知状态变化?如果答案清晰,代码自然会更稳。

相关文章
|
4天前
|
云安全 人工智能 运维
阿里云SecOps Agent,全新安全跨产品执行体验
自然语言驱动 云安全中心/WAF/CFW/ 等多款安全产品联动
1595 2
|
1天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
348 122
|
4天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
577 3
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
14天前
|
缓存 测试技术 API
Qwen 3.7 Plus 与 Max 实测:性价比与多模态能力差异解析(2026)
2026 年 6 月 1 日,阿里悄无声息地发布了 Qwen 3.7 Plus,距 Qwen 3.7 Max 上线刚好 11 天。同样的 1M 上下文,同样的 35 小时自治上限。但价格才是头条:Plus 是 0.40/M输入,Max是 2.50/M——便宜约 6 倍——并且还能看图、看视频。Vision Arena 上 Plus 已经排到 #16。所以这周真正值得讨论的问题不是”要不要为视觉能力买单”,而是”Max 凭什么用 6 倍价格换来 2 个百分点的 benchmark 领先”。
|
15天前
|
JavaScript 定位技术 API
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
CodeGraph 是一款爆火的本地代码智能工具,通过 tree-sitter 解析 AST 构建结构化知识图谱(存于 SQLite),为编程 Agent 提前生成“代码地图”。它显著降低 Agent 在中大型项目中的探索成本——实测工具调用减少71%、Token 降57%、速度提升46%,支持19+语言及主流框架路由识别,完全离线、无需 API Key。
910 11
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
|
8天前
|
缓存 人工智能 运维
GLM 5.2自托管全流程实战:硬件选型、vLLM/SGLang部署与成本盈亏测算
2026年智谱发布GLM 5.2超大混合专家模型,区别于以往仅开放API的闭源大模型,该模型权重以MIT开源协议对外发布,企业与开发者可完整下载、本地审计、私有化部署,实现数据不出环境、自定义微调、自主调度推理资源。GLM 5.2拥有753B总参数,原生支持百万级上下文窗口,在代码生成、长文档推理、数学逻辑等多项基准测试中对标国际顶尖商用模型,是首款可完整自托管的前沿代码向大模型。
651 0
|
2天前
|
消息中间件 人工智能 Kafka
AI 时代,实时入湖正在告别 ETL:从 Kafka 到 Iceberg 的架构减法
本文围绕“零 ETL”这一趋势,讨论流数据入湖为什么需要做架构减法,并结合 Kafka × Table Bucket 的实践,分析一种将通用入湖能力前移到消息与表存储链路中的方案,如何在降低复杂度的同时,兼顾实时性、一致性、Schema 演进、CDC 语义与开放生态兼容。
192 121
|
2天前
|
人工智能 监控 前端开发
Electron 监控:让桌面 Agent 监控触手可及
一行代码实现Electron桌面端全景监控,自动还原崩溃现场、预警内存泄漏、全链路追踪、 SSE流式响应与交互埋点,让 AI 助手运行状态清晰可见,助力快速恢复稳定与流畅。
182 125
|
11天前
|
人工智能 自然语言处理 算法
阿里云百炼Qwen 3.7 Plus与Max实测全解:性价比与多模态能力、成本深度对比
2026年,阿里云百炼平台推出的Qwen 3.7系列成为企业与开发者落地AI应用的核心选择,其中Qwen 3.7 Max与Plus作为两大旗舰版本,定位差异显著:Max是纯文本推理旗舰,专注高强度智能体与复杂逻辑任务;Plus则是多模态全能版,在保留强大文本能力的同时,补齐图像、视频理解能力,且价格大幅降低。本文基于2026年最新实测数据,从核心参数、文本能力、多模态能力、智能体表现、性价比与场景选型六大维度,全面解析两款模型的差异,为用户提供精准选型参考。
537 0