当前位置: 首页 > news >正文

Android Jetpack 核心组件实战:ViewModel + LiveData + DataBinding 详解

Android Jetpack 核心组件实战:ViewModel + LiveData + DataBinding 详解

在 Android 开发中,我们经常会遇到屏幕旋转数据丢失UI 与逻辑耦合紧密数据更新无法自动同步 UI 等问题。Google 推出的 Jetpack 架构组件可以很好地解决这些问题,本文将对 ViewModelLiveDataDataBinding 三个核心组件进行讲解,从基础概念到实战案例,完整讲解这三个组件的使用方法与联动逻辑。

一、ViewModel:解决配置变更数据丢失问题

1. 为什么需要 ViewModel?

当 Activity 因屏幕旋转内存回收等配置变更被销毁重建时,Activity 中的数据(如计数器、网络请求结果)会随之丢失。如果在 Activity 中直接处理数据,不仅会导致重复加载(如重复发起网络请求),还会造成用户体验差、性能浪费等问题。

ViewModel 的核心作用就是:存储和管理与 UI 相关的数据,且生命周期独立于 Activity/Fragment 的重建。即使页面重建,ViewModel 中的数据依然保留,新页面可直接复用。

2. 添加依赖

app/build.gradledependencies 块中添加 ViewModel 依赖(最新版本可在 Jetpack 官网 查询):

dependencies {// ViewModel 核心依赖implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0"
}

3. 定义 ViewModel 类

ViewModel 需继承 ViewModel 基类,内部存储需要保留的数据,并提供操作数据的方法。以“计数器”为例:

import androidx.lifecycle.ViewModelclass CounterViewModel : ViewModel() {// 存储计数数据(初始值为 0)private var counter = 0// 获取当前计数fun getCounter(): Int = counter// 计数 +1fun plusOne() {counter++}// 重置计数fun clear() {counter = 0}
}
  • 注意:ViewModel 中不要持有 Activity/Fragment 的引用(如 Context),否则会因生命周期不匹配导致内存泄漏。若需 Context,可使用 AndroidViewModel(需传入 Application)。

4. 在 Activity 中使用 ViewModel

通过 ViewModelProvider 获取 ViewModel 实例(而非直接 new),确保配置变更后获取的是同一个实例:

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import android.widget.Button
import android.widget.TextViewclass MainActivity : AppCompatActivity() {private lateinit var viewModel: CounterViewModelprivate lateinit var tvCounter: TextViewprivate lateinit var btnAdd: Buttonprivate lateinit var btnClear: Buttonoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// 初始化 UI 组件tvCounter = findViewById(R.id.tv_counter)btnAdd = findViewById(R.id.btn_add)btnClear = findViewById(R.id.btn_clear)// 关键:通过 ViewModelProvider 获取 ViewModel 实例// this 表示当前 Activity(ViewModel 的作用域)viewModel = ViewModelProvider(this)[CounterViewModel::class.java]// 初始显示计数updateCounterUI()// 点击“+1”按钮btnAdd.setOnClickListener {viewModel.plusOne()updateCounterUI() // 手动更新 UI}// 点击“重置”按钮btnClear.setOnClickListener {viewModel.clear()updateCounterUI() // 手动更新 UI}}// 手动更新 TextView 显示private fun updateCounterUI() {tvCounter.text = "当前计数:${viewModel.getCounter()}"}
}

效果:点击按钮后计数增加,旋转屏幕后计数不会归零——ViewModel 成功保留了数据。

5. ViewModel.Factory:处理带参构造的 ViewModel

上述 ViewModel 是无参构造的,但实际开发中常需要传入初始值(如从 SharedPreferences 读取的历史计数)。此时需自定义 ViewModel.Factory 来创建带参 ViewModel。

  • 步骤 1:定义 Factory 类
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider// 接收初始计数作为参数
class CounterViewModelFactory(private val initialCount: Int) : ViewModelProvider.Factory {// 重写 create 方法,创建带参 ViewModeloverride fun <T : ViewModel> create(modelClass: Class<T>): T {// 检查 modelClass 是否为 CounterViewModel 类型if (modelClass.isAssignableFrom(CounterViewModel::class.java)) {// 传入初始值创建实例return CounterViewModel(initialCount) as T}throw IllegalArgumentException("未知的 ViewModel 类")}
}
  • 步骤 2:修改 ViewModel 支持带参构造
class CounterViewModel(private val initialCount: Int) : ViewModel() {private var counter = initialCount // 用初始值初始化// 其余方法不变...
}
  • 步骤 3:通过 Factory 获取 ViewModel
// 从 SharedPreferences 读取历史计数(示例)
val sp = getPreferences(MODE_PRIVATE)
val savedCount = sp.getInt("saved_count", 0)// 通过 Factory 传入初始值
viewModel = ViewModelProvider(this,CounterViewModelFactory(savedCount) // 传入初始计数
)[CounterViewModel::class.java]

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 修改数据

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelclass CounterViewModel(initialCount: Int) : ViewModel() {// 私有可变 LiveData(内部修改)private val _counter = MutableLiveData<Int>()// 公开不可变 LiveData(外部仅能观察)val counter: LiveData<Int> = _counterinit {// 初始化数据(LiveData 通过 value 存取值)_counter.value = initialCount}fun plusOne() {// 获取当前值(为空时默认 0)val currentCount = _counter.value ?: 0_counter.value = currentCount + 1 // 主线程更新数据}fun clear() {_counter.value = 0}// 非主线程更新数据(如子线程网络请求后)fun postCount(newCount: Int) {_counter.postValue(newCount) // 内部会切换到主线程}
}
  • setValue() vs postValue()
    • setValue():仅能在主线程调用,立即更新数据。
    • postValue():可在子线程调用,通过 Handler 切换到主线程更新数据(适合网络请求、数据库操作等异步场景)。
步骤 2:在 Activity 中观察 LiveData

通过 LiveData.observe() 注册观察者,数据变化时自动回调 onChanged 方法:

class MainActivity : AppCompatActivity() {private lateinit var viewModel: CounterViewModelprivate lateinit var tvCounter: TextViewprivate lateinit var btnAdd: Buttonprivate lateinit var btnClear: Buttonprivate lateinit var sp: SharedPreferencesoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// 初始化 SharedPreferences(用于保存历史计数)sp = getPreferences(MODE_PRIVATE)val savedCount = sp.getInt("saved_count", 0)// 通过 Factory 获取带参 ViewModelviewModel = ViewModelProvider(this,CounterViewModelFactory(savedCount))[CounterViewModel::class.java]// 初始化 UI 组件tvCounter = findViewById(R.id.tv_counter)btnAdd = findViewById(R.id.btn_add)btnClear = findViewById(R.id.btn_clear)// 关键:观察 LiveData 变化(自动更新 UI)viewModel.counter.observe(this) { newCount ->// 数据变化时回调,更新 TextViewtvCounter.text = "当前计数:$newCount"}// 点击“+1”按钮(仅操作数据,无需手动更新 UI)btnAdd.setOnClickListener {viewModel.plusOne()}// 点击“重置”按钮btnClear.setOnClickListener {viewModel.clear()}}// 页面暂停时保存计数到 SharedPreferencesoverride fun onPause() {super.onPause()sp.edit().putInt("saved_count", viewModel.counter.value ?: 0).apply()}
}

核心变化:移除了 updateCounterUI() 手动刷新逻辑,LiveData 自动将数据变化同步到 UI。

3. LiveData 高级用法:map 与 switchMap

(1)map:数据转换

当需要将 LiveData 中的原始数据转换为 UI 所需的格式时,使用 map()。例如:将 User 对象转换为“姓名+年龄”的字符串。

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.lifecycle.ViewModel// 原始数据类
data class User(val name: String, val age: Int)class UserViewModel : ViewModel() {// 原始 LiveData(存储 User 对象)private val _user = MutableLiveData<User>()// 转换后的 LiveData(仅暴露姓名+年龄字符串)val userInfo: LiveData<String> = _user.map { user ->"${user.name}${user.age}岁)"}// 模拟更新用户数据fun updateUser(newUser: User) {_user.value = newUser}
}

在 Activity 中观察 userInfo,即可直接获取转换后的字符串:

viewModel.userInfo.observe(this) { info ->tvUserInfo.text = info // 直接显示“张三(20岁)”
}
(2)switchMap:动态切换 LiveData

当 LiveData 的数据源需要动态切换时(如根据用户 ID 加载不同用户数据),使用 switchMap()。例如:根据随机生成的 userId 加载对应的 User

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SwitchMap
import androidx.lifecycle.ViewModel// 模拟数据仓库(如网络请求、数据库查询)
object UserRepository {// 根据 userId 获取 User(模拟耗时操作)fun getUserById(userId: String): LiveData<User> {val liveData = MutableLiveData<User>()liveData.value = User("用户$userId", userId.toInt() % 30 + 18) // 模拟年龄return liveData}
}class UserViewModel : ViewModel() {// 触发数据源切换的“开关”LiveData(存储 userId)private val _userId = MutableLiveData<String>()// 动态切换的 LiveData(根据 userId 加载不同 User)val user: LiveData<User> = _userId.switchMap { userId ->UserRepository.getUserById(userId)}// 对外提供方法,更新 userId(触发数据源切换)fun loadUser(userId: String) {_userId.value = userId}
}

在 Activity 中点击按钮切换 userIduser 会自动加载新数据并更新 UI:

// 初始加载随机用户
viewModel.loadUser((0..1000).random().toString())// 点击“切换用户”按钮
btnSwitchUser.setOnClickListener {val newUserId = (0..1000).random().toString()viewModel.loadUser(newUserId)
}// 观察用户数据变化
viewModel.user.observe(this) { user ->tvUserInfo.text = "${user.name}${user.age}岁)"
}

三、DataBinding:消除模板代码,实现 UI 与数据绑定

1. 为什么需要 DataBinding?

传统开发中,我们需要通过 findViewById 获取 UI 组件引用,再手动设置数据(如 textonClick),代码冗余且耦合度高。DataBinding 是一种数据绑定库,可直接在 XML 中关联数据与 UI,消除模板代码,实现“XML 绑定数据,数据驱动 UI”。

2. 启用 DataBinding

app/build.gradleandroid 块中开启 DataBinding:

android {...buildFeatures {dataBinding = true // 启用 DataBinding}
}

3. 实战:ViewModel + LiveData + DataBinding 联动

下面以一个简单的计数器的例子结合着ViewModel和LiveData使用。先看一下这个VIewModel类,就是个简单的计数器

class CounterViewModel: ViewModel() {val counter : LiveData<Int>get() = _counterprivate val _counter = MutableLiveData<Int>()init {_counter.value = count}fun plusOne() {val count = counter.value ?: 0_counter.value = count + 1}fun clear() {_counter.value = 0}}

接着修改xml文件,dataBinding绑定的布局文件的根布局必须使用layout,只能存在一个<data>作用域和一个直接view子节点(比如)

在data作用域中声明数据变量,然后在对应的ui组件中添加组件的指向属性(例如下面TextView的android:text="@{计数: + String.valueOf(vm.counter)}")

然后还在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)。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><data><variablename="myCount"type="int" /><variablename="vm"type="com.example.databindingtest.CounterViewModel" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:id="@+id/button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Button"app:layout_constraintTop_toTopOf="parent"android:layout_marginTop="15dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"android:onClick="@{() -> vm.plusOne()}"/><TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{`计数:` + String.valueOf(vm.counter)}"android:textSize="28sp"app:layout_constraintTop_toBottomOf="@id/button"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
</layout>

最后在对应的活动文件中通过 DataBinding 生成的绑定类,将数据与布局关联。首先通过 DataBindingUtil.setContentView() 方法,将 XML 布局文件 (R.layout.activity_main) 填充并解析,生成一个对应的绑定类(例如 ActivityMainBinding)的实例并得到返回的binding对象,将活动的viewModel给dataBinding的viewModel,然后显式绑定当前页面和databinding的生命周期

class MainActivity : AppCompatActivity() {private lateinit var binding : ActivityMainBindinglateinit var viewModel : CounterViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.vm = viewModelbinding.lifecycleOwner = this}
}

这里解释一下binding.lifecycleOwner = this,这是在告诉 DataBinding:当前页面的生命周期 = 这个 Activity 的生命周期。我们可能不太理解,明明都在 Activity 里了,为什么还要多此一举告诉它这是 Activity?这是因为DataBinding 不是 Activity 的一部分,它只是一个独立生成的“绑定类对象”,默认不知道要跟谁的生命周期同步。我们必须手动告诉它:LiveData 的观察者应该跟随这个 Activity(或 Fragment)销毁时自动解除订阅。

这样就完成了一个简单的DataBinding、ViewModel和LiveData的联动了

http://www.dtcms.com/a/441782.html

相关文章:

  • 商务厅网站建设意见怎么做网站注册推广
  • Fragment 崩溃恢复后出现重叠问题的复现方式
  • 设计模式(C++)详解——策略模式(2)
  • 使客户能够大规模交付生产就绪的人工智能代理
  • Layui 前端和 PHP 后端的大视频分片上传方案
  • 无状态HTTP的“记忆”方案:Spring Boot中CookieSession全栈实战
  • Java 内存模型(JMM)面试清单(含超通俗生活案例与深度理解)
  • 2015网站建设专业建网站设计公司
  • vue+springboot项目部署到服务器
  • QT肝8天17--优化用户管理
  • QT肝8天19--Windows程序部署
  • 【开题答辩过程】以《基于 Spring Boot 的宠物应急救援系统设计与实现》为例,不会开题答辩的可以进来看看
  • 成都seo网站建设沈阳网站建设推广服务
  • 网站栏目名短链接在线生成官网免费
  • Task Schemas: 基于前沿认知的复杂推理任务架构
  • 第三十七章 ESP32S3 SPI_SDCARD 实验
  • 企业营销型网站特点企业信息查询系统官网山东省
  • docker-compose 安装MySQL8.0.39
  • Go语言入门(18)-指针(上)
  • Django ORM - 聚合查询
  • 【STM32项目开源】基于STM32的智能老人拐杖
  • YOLO入门教程(番外):卷积神经网络—汇聚层
  • 网站改版一般需要多久智慧团建学生登录入口
  • Dotnet接入AI通过Response创建一个简单控制台案例
  • 【论文笔记】2025年图像处理顶会论文
  • 用 Maven 配置 Flink 从初始化到可部署的完整实践
  • 做职业规划的网站seo学院
  • 怎么建优惠券网站太原seo排名外包
  • jmeter中java.net.ConnectException: Connection refused: connect
  • “十四五”科技冲锋:迈向科技强国的壮阔征程