Android Jetpack 核心组件实战:ViewModel + LiveData + DataBinding 详解
在 Android 开发中,我们经常会遇到屏幕旋转数据丢失、UI 与逻辑耦合紧密、数据更新无法自动同步 UI 等问题。Google 推出的 Jetpack 架构组件可以很好地解决这些问题,本文将对 ViewModel、LiveData 和 DataBinding 三个核心组件进行讲解,从基础概念到实战案例,完整讲解这三个组件的使用方法与联动逻辑。
一、ViewModel:解决配置变更数据丢失问题
1. 为什么需要 ViewModel?
当 Activity 因屏幕旋转、内存回收等配置变更被销毁重建时,Activity 中的数据(如计数器、网络请求结果)会随之丢失。如果在 Activity 中直接处理数据,不仅会导致重复加载(如重复发起网络请求),还会造成用户体验差、性能浪费等问题。
ViewModel 的核心作用就是:存储和管理与 UI 相关的数据,且生命周期独立于 Activity/Fragment 的重建。即使页面重建,ViewModel 中的数据依然保留,新页面可直接复用。
2. 添加依赖
在 app/build.gradle 的 dependencies 块中添加 ViewModel 依赖(最新版本可在 Jetpack 官网 查询):
1dependencies { 2 // ViewModel 核心依赖 3 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0" 4} 5
3. 定义 ViewModel 类
ViewModel 需继承 ViewModel 基类,内部存储需要保留的数据,并提供操作数据的方法。以“计数器”为例:
1import androidx.lifecycle.ViewModel 2 3class CounterViewModel : ViewModel() { 4 // 存储计数数据(初始值为 0) 5 private var counter = 0 6 7 // 获取当前计数 8 fun getCounter(): Int = counter 9 10 // 计数 +1 11 fun plusOne() { 12 counter++ 13 } 14 15 // 重置计数 16 fun clear() { 17 counter = 0 18 } 19} 20
- 注意:ViewModel 中不要持有 Activity/Fragment 的引用(如
Context),否则会因生命周期不匹配导致内存泄漏。若需Context,可使用AndroidViewModel(需传入Application)。
4. 在 Activity 中使用 ViewModel
通过 ViewModelProvider 获取 ViewModel 实例(而非直接 new),确保配置变更后获取的是同一个实例:
1import androidx.appcompat.app.AppCompatActivity 2import androidx.lifecycle.ViewModelProvider 3import android.os.Bundle 4import android.widget.Button 5import android.widget.TextView 6 7class MainActivity : AppCompatActivity() { 8 private lateinit var viewModel: CounterViewModel 9 private lateinit var tvCounter: TextView 10 private lateinit var btnAdd: Button 11 private lateinit var btnClear: Button 12 13 override fun onCreate(savedInstanceState: Bundle?) { 14 super.onCreate(savedInstanceState) 15 setContentView(R.layout.activity_main) 16 17 // 初始化 UI 组件 18 tvCounter = findViewById(R.id.tv_counter) 19 btnAdd = findViewById(R.id.btn_add) 20 btnClear = findViewById(R.id.btn_clear) 21 22 // 关键:通过 ViewModelProvider 获取 ViewModel 实例 23 // this 表示当前 Activity(ViewModel 的作用域) 24 viewModel = ViewModelProvider(this)[CounterViewModel::class.java] 25 26 // 初始显示计数 27 updateCounterUI() 28 29 // 点击“+1”按钮 30 btnAdd.setOnClickListener { 31 viewModel.plusOne() 32 updateCounterUI() // 手动更新 UI 33 } 34 35 // 点击“重置”按钮 36 btnClear.setOnClickListener { 37 viewModel.clear() 38 updateCounterUI() // 手动更新 UI 39 } 40 } 41 42 // 手动更新 TextView 显示 43 private fun updateCounterUI() { 44 tvCounter.text = "当前计数:${viewModel.getCounter()}" 45 } 46} 47
效果:点击按钮后计数增加,旋转屏幕后计数不会归零——ViewModel 成功保留了数据。
5. ViewModel.Factory:处理带参构造的 ViewModel
上述 ViewModel 是无参构造的,但实际开发中常需要传入初始值(如从 SharedPreferences 读取的历史计数)。此时需自定义 ViewModel.Factory 来创建带参 ViewModel。
- 步骤 1:定义 Factory 类
1import androidx.lifecycle.ViewModel 2import androidx.lifecycle.ViewModelProvider 3 4// 接收初始计数作为参数 5class CounterViewModelFactory(private val initialCount: Int) : ViewModelProvider.Factory { 6 // 重写 create 方法,创建带参 ViewModel 7 override fun <T : ViewModel> create(modelClass: Class<T>): T { 8 // 检查 modelClass 是否为 CounterViewModel 类型 9 if (modelClass.isAssignableFrom(CounterViewModel::class.java)) { 10 // 传入初始值创建实例 11 return CounterViewModel(initialCount) as T 12 } 13 throw IllegalArgumentException("未知的 ViewModel 类") 14 } 15} 16
- 步骤 2:修改 ViewModel 支持带参构造
1class CounterViewModel(private val initialCount: Int) : ViewModel() { 2 private var counter = initialCount // 用初始值初始化 3 4 // 其余方法不变... 5} 6
- 步骤 3:通过 Factory 获取 ViewModel
1// 从 SharedPreferences 读取历史计数(示例) 2val sp = getPreferences(MODE_PRIVATE) 3val savedCount = sp.getInt("saved_count", 0) 4 5// 通过 Factory 传入初始值 6viewModel = ViewModelProvider( 7 this, 8 CounterViewModelFactory(savedCount) // 传入初始计数 9)[CounterViewModel::class.java] 10
6. ViewModel 生命周期关键点
- 创建时机:首次调用
ViewModelProvider.get()时创建。 - 销毁时机:仅当 Activity/Fragment 被永久销毁(如用户按返回键)时,系统才会调用
ViewModel.onCleared()释放资源(可在此处取消网络请求、解绑观察者等)。 - 作用域:同一个
ViewModelStoreOwner(如 Activity、Fragment)内,同一类型的 ViewModel 仅存在一个实例。
二、LiveData:实现数据变化自动更新 UI
1. 为什么需要 LiveData?
ViewModel 解决了数据保留问题,但无法主动将数据变化通知给 UI——上述案例中,我们需要手动调用 updateCounterUI() 刷新界面。若数据更新频繁(如实时定位、网络流),手动更新会导致代码冗余且易出错。
LiveData 是一种可观察的数据容器,核心能力:
- 生命周期感知:自动跟随 Activity/Fragment 的生命周期,仅在页面活跃时(
onStart后)通知数据变化,避免内存泄漏。 - 自动通知 UI:数据更新时,自动回调观察者,无需手动刷新 UI。
2. LiveData 基础用法(结合 ViewModel)
步骤 1:用 LiveData 包装数据
LiveData 是抽象类,通常使用其子类 MutableLiveData(支持修改数据)。为保证封装性,对外暴露不可变的 LiveData,内部用 MutableLiveData 修改数据:
1import androidx.lifecycle.LiveData 2import androidx.lifecycle.MutableLiveData 3import androidx.lifecycle.ViewModel 4 5class CounterViewModel(initialCount: Int) : ViewModel() { 6 // 私有可变 LiveData(内部修改) 7 private val _counter = MutableLiveData<Int>() 8 // 公开不可变 LiveData(外部仅能观察) 9 val counter: LiveData<Int> = _counter 10 11 init { 12 // 初始化数据(LiveData 通过 value 存取值) 13 _counter.value = initialCount 14 } 15 16 fun plusOne() { 17 // 获取当前值(为空时默认 0) 18 val currentCount = _counter.value ?: 0 19 _counter.value = currentCount + 1 // 主线程更新数据 20 } 21 22 fun clear() { 23 _counter.value = 0 24 } 25 26 // 非主线程更新数据(如子线程网络请求后) 27 fun postCount(newCount: Int) { 28 _counter.postValue(newCount) // 内部会切换到主线程 29 } 30} 31
setValue()vspostValue():setValue():仅能在主线程调用,立即更新数据。postValue():可在子线程调用,通过 Handler 切换到主线程更新数据(适合网络请求、数据库操作等异步场景)。
步骤 2:在 Activity 中观察 LiveData
通过 LiveData.observe() 注册观察者,数据变化时自动回调 onChanged 方法:
1class MainActivity : AppCompatActivity() { 2 private lateinit var viewModel: CounterViewModel 3 private lateinit var tvCounter: TextView 4 private lateinit var btnAdd: Button 5 private lateinit var btnClear: Button 6 private lateinit var sp: SharedPreferences 7 8 override fun onCreate(savedInstanceState: Bundle?) { 9 super.onCreate(savedInstanceState) 10 setContentView(R.layout.activity_main) 11 12 // 初始化 SharedPreferences(用于保存历史计数) 13 sp = getPreferences(MODE_PRIVATE) 14 val savedCount = sp.getInt("saved_count", 0) 15 16 // 通过 Factory 获取带参 ViewModel 17 viewModel = ViewModelProvider( 18 this, 19 CounterViewModelFactory(savedCount) 20 )[CounterViewModel::class.java] 21 22 // 初始化 UI 组件 23 tvCounter = findViewById(R.id.tv_counter) 24 btnAdd = findViewById(R.id.btn_add) 25 btnClear = findViewById(R.id.btn_clear) 26 27 // 关键:观察 LiveData 变化(自动更新 UI) 28 viewModel.counter.observe(this) { newCount -> 29 // 数据变化时回调,更新 TextView 30 tvCounter.text = "当前计数:$newCount" 31 } 32 33 // 点击“+1”按钮(仅操作数据,无需手动更新 UI) 34 btnAdd.setOnClickListener { 35 viewModel.plusOne() 36 } 37 38 // 点击“重置”按钮 39 btnClear.setOnClickListener { 40 viewModel.clear() 41 } 42 } 43 44 // 页面暂停时保存计数到 SharedPreferences 45 override fun onPause() { 46 super.onPause() 47 sp.edit().putInt("saved_count", viewModel.counter.value ?: 0).apply() 48 } 49} 50
核心变化:移除了 updateCounterUI() 手动刷新逻辑,LiveData 自动将数据变化同步到 UI。
3. LiveData 高级用法:map 与 switchMap
(1)map:数据转换
当需要将 LiveData 中的原始数据转换为 UI 所需的格式时,使用 map()。例如:将 User 对象转换为“姓名+年龄”的字符串。
1import androidx.lifecycle.LiveData 2import androidx.lifecycle.MutableLiveData 3import androidx.lifecycle.map 4import androidx.lifecycle.ViewModel 5 6// 原始数据类 7data class User(val name: String, val age: Int) 8 9class UserViewModel : ViewModel() { 10 // 原始 LiveData(存储 User 对象) 11 private val _user = MutableLiveData<User>() 12 // 转换后的 LiveData(仅暴露姓名+年龄字符串) 13 val userInfo: LiveData<String> = _user.map { user -> 14 "${user.name}(${user.age}岁)" 15 } 16 17 // 模拟更新用户数据 18 fun updateUser(newUser: User) { 19 _user.value = newUser 20 } 21} 22
在 Activity 中观察 userInfo,即可直接获取转换后的字符串:
1viewModel.userInfo.observe(this) { info -> 2 tvUserInfo.text = info // 直接显示“张三(20岁)” 3} 4
(2)switchMap:动态切换 LiveData
当 LiveData 的数据源需要动态切换时(如根据用户 ID 加载不同用户数据),使用 switchMap()。例如:根据随机生成的 userId 加载对应的 User。
1import androidx.lifecycle.LiveData 2import androidx.lifecycle.MutableLiveData 3import androidx.lifecycle.SwitchMap 4import androidx.lifecycle.ViewModel 5 6// 模拟数据仓库(如网络请求、数据库查询) 7object UserRepository { 8 // 根据 userId 获取 User(模拟耗时操作) 9 fun getUserById(userId: String): LiveData<User> { 10 val liveData = MutableLiveData<User>() 11 liveData.value = User("用户$userId", userId.toInt() % 30 + 18) // 模拟年龄 12 return liveData 13 } 14} 15 16class UserViewModel : ViewModel() { 17 // 触发数据源切换的“开关”LiveData(存储 userId) 18 private val _userId = MutableLiveData<String>() 19 20 // 动态切换的 LiveData(根据 userId 加载不同 User) 21 val user: LiveData<User> = _userId.switchMap { userId -> 22 UserRepository.getUserById(userId) 23 } 24 25 // 对外提供方法,更新 userId(触发数据源切换) 26 fun loadUser(userId: String) { 27 _userId.value = userId 28 } 29} 30
在 Activity 中点击按钮切换 userId,user 会自动加载新数据并更新 UI:
1// 初始加载随机用户 2viewModel.loadUser((0..1000).random().toString()) 3 4// 点击“切换用户”按钮 5btnSwitchUser.setOnClickListener { 6 val newUserId = (0..1000).random().toString() 7 viewModel.loadUser(newUserId) 8} 9 10// 观察用户数据变化 11viewModel.user.observe(this) { user -> 12 tvUserInfo.text = "${user.name}(${user.age}岁)" 13} 14
三、DataBinding:消除模板代码,实现 UI 与数据绑定
1. 为什么需要 DataBinding?
传统开发中,我们需要通过 findViewById 获取 UI 组件引用,再手动设置数据(如 text、onClick),代码冗余且耦合度高。DataBinding 是一种数据绑定库,可直接在 XML 中关联数据与 UI,消除模板代码,实现“XML 绑定数据,数据驱动 UI”。
2. 启用 DataBinding
在 app/build.gradle 的 android 块中开启 DataBinding:
1android { 2 ... 3 buildFeatures { 4 dataBinding = true // 启用 DataBinding 5 } 6} 7
3. 实战:ViewModel + LiveData + DataBinding 联动
下面以一个简单的计数器的例子结合着ViewModel和LiveData使用。先看一下这个VIewModel类,就是个简单的计数器
1class CounterViewModel: ViewModel() { 2 val counter : LiveData<Int> 3 get() = _counter 4 5 private val _counter = MutableLiveData<Int>() 6 7 init { 8 _counter.value = count 9 } 10 fun plusOne() { 11 val count = counter.value ?: 0 12 _counter.value = count + 1 13 } 14 15 fun clear() { 16 _counter.value = 0 17 } 18 19 20} 21
接着修改xml文件,dataBinding绑定的布局文件的根布局必须使用layout,只能存在一个<data>作用域和一个直接view子节点(比如)
在data作用域中声明数据变量,然后在对应的ui组件中添加组件的指向属性(例如下面TextView的[android:text="@{计数: + String.valueOf(vm.counter)}"](function valueOf() { [native code] }))
然后还在Button的点击事件上绑定了ViewModel中的pulsOne()方法,这样每次点击视图就会自动给ViewModel中的value+1,然后TextView中的counter也会+1,实现了双向绑定。不过注意,TextView中绑定的是vm的counter,这是一个LiveData,也就是容器,真正存值的是LiveData的value,但是DataBinding 对 LiveData 有特殊支持:当你在 XML 中写 @{vm.counter} 时,DataBinding 会自动观察这个 LiveData 的变化,并且直接使用它的 value 值(相当于在代码中获取 counter.value)。
1<?xml version="1.0" encoding="utf-8"?> 2<layout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:tools="http://schemas.android.com/tools"> 5 6 <data> 7 <variable 8 name="myCount" 9 type="int" /> 10 <variable 11 name="vm" 12 type="com.example.databindingtest.CounterViewModel" /> 13 </data> 14 15 <androidx.constraintlayout.widget.ConstraintLayout 16 android:id="@+id/main" 17 android:layout_width="match_parent" 18 android:layout_height="match_parent" 19 tools:context=".MainActivity"> 20 21 <Button 22 android:id="@+id/button" 23 android:layout_width="wrap_content" 24 android:layout_height="wrap_content" 25 android:text="Button" 26 app:layout_constraintTop_toTopOf="parent" 27 android:layout_marginTop="15dp" 28 app:layout_constraintStart_toStartOf="parent" 29 app:layout_constraintEnd_toEndOf="parent" 30 android:onClick="@{() -> vm.plusOne()}" 31 /> 32 33 <TextView 34 android:id="@+id/textView" 35 android:layout_width="wrap_content" 36 android:layout_height="wrap_content" 37 android:text="@{`计数:` + String.valueOf(vm.counter)}" 38 android:textSize="28sp" 39 app:layout_constraintTop_toBottomOf="@id/button" 40 app:layout_constraintStart_toStartOf="parent" 41 app:layout_constraintEnd_toEndOf="parent" /> 42 43 44 </androidx.constraintlayout.widget.ConstraintLayout> 45</layout> 46 47
最后在对应的活动文件中通过 DataBinding 生成的绑定类,将数据与布局关联。首先通过 DataBindingUtil.setContentView() 方法,将 XML 布局文件 (R.layout.activity_main) 填充并解析,生成一个对应的绑定类(例如 ActivityMainBinding)的实例并得到返回的binding对象,将活动的viewModel给dataBinding的viewModel,然后显式绑定当前页面和databinding的生命周期
1class MainActivity : AppCompatActivity() { 2 private lateinit var binding : ActivityMainBinding 3 lateinit var viewModel : CounterViewModel 4 override fun onCreate(savedInstanceState: Bundle?) { 5 super.onCreate(savedInstanceState) 6 enableEdgeToEdge() 7 viewModel = ViewModelProvider(this).get(CounterViewModel::class.java) 8 binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 9 binding.vm = viewModel 10 binding.lifecycleOwner = this 11 } 12} 13
这里解释一下binding.lifecycleOwner = this,这是在告诉 DataBinding:当前页面的生命周期 = 这个 Activity 的生命周期。我们可能不太理解,明明都在 Activity 里了,为什么还要多此一举告诉它这是 Activity?这是因为DataBinding 不是 Activity 的一部分,它只是一个独立生成的“绑定类对象”,默认不知道要跟谁的生命周期同步。我们必须手动告诉它:LiveData 的观察者应该跟随这个 Activity(或 Fragment)销毁时自动解除订阅。
这样就完成了一个简单的DataBinding、ViewModel和LiveData的联动了
《Android Jetpack 核心组件实战:ViewModel + LiveData + DataBinding 详解》 是转载文章,点击查看原文。
