探究Jetpack(二)之LiveData

news2025/1/12 13:14:33

目录

  • LiveData的基本用法
  • 比较安全的LiveData使用方法
  • map和switchMap
    • map
    • switchMap

LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者
LiveData特别适合与ViewModel结合在一起使用,虽然它也可以单独用在别的地方,但是在绝大多数情况下,它是使用在ViewModel当中的

LiveData的基本用法

之前编写的那个计数器虽然功能非常简单,但其实是存在问题的。目前的逻辑是,当每次点击“Plus One”按钮时,都会先给ViewModel中的计数加1,然后立即获取最新的计数
这种方式在单线程模式下确实可以正常工作,但如果ViewModel的内部开启了线程去执行一些耗时逻辑,那么在点击按钮后就立即去获取最新的数据,得到的肯定还是之前的数据

而这个问题的解决方案也是显而易见的,就是使用LiveData。正如前面所描述的一样,LiveData可以包含任何类型的数据,并在数据发生变化的时候通知给观察者
也就是说,如果将计数器的计数使用LiveData来包装,然后在Activity中去观察它,就可以主动将数据变化通知给Activity了

修改MainViewModel中的代码,如下所示:

class MainViewModel(countReserved: Int) : ViewModel() {

	 val counter = MutableLiveData<Int>()
	 
	 init {
	 	counter.value = countReserved
	 }
	 
	 fun plusOne() {
		 val count = counter.value ?: 0
		 counter.value = count + 1
	 }
	 
	 fun clear() {
	 	counter.value = 0
	 }
}

这里将counter变量修改成了一个MutableLiveData对象,并指定它的泛型为Int,表示它包含的是整型数据
MutableLiveData是一种可变的LiveData,它的用法很简单,主要有3种读写数据的方法,分别是getValue()、setValue()和postValue()方法

  • getValue()方法用于获取LiveData中包含的数据
  • setValue()方法用于给LiveData设置数据,但是只能在主线程中调用
  • postValue()方法用于在非主线程中给LiveData设置数据

而上述代码其实就是调用getValue()和setValue()方法对应的语法糖写法
可以看到,这里在init结构体中给counter设置数据,这样之前保存的计数值就可以在初始化的时候得到恢复
接下来我们新增了plusOne()和clear()这两个方法,分别用于给计数加1以及将计数清零。plusOne()方法中的逻辑是先获取counter中包含的数据,然后给它加1,再重新设置到counter当中
注意调用LiveData的getValue()方法所获得的数据是可能为空的,因此这里使用了一个?:操作符,当获取到的数据为空时,就用0来作为默认计数

修改MainActivity.xml的代码

class MainActivity : AppCompatActivity() {
	 ...
	 override fun onCreate(savedInstanceState: Bundle?) {
		 ...
		 plusOneBtn.setOnClickListener {
		 	viewModel.plusOne()
		 }
		 clearBtn.setOnClickListener {
		 	viewModel.clear()
		 }
		 viewModel.counter.observe(this) { count ->
		 	infoText.text = count.toString()
		 }
	 }
	 
	 override fun onPause() {
		 super.onPause()
		 sp.edit {
		 	putInt("count_reserved", viewModel.counter.value ?: 0)
		 }
	 }
}

在“Plus One”按钮的点击事件中应该去调用MainViewModel的plusOne()方法,而在“Clear”按钮的点击事件中应该去调用MainViewModel的clear()方法
另外,在onPause()方法中,将获取当前计数的写法改造了一下,这部分内容还是很好理解的
接下来到最关键的地方了,这里调用了viewModel.counter的observe()方法来观察数据的变化。经过对MainViewModel的改造,现在counter变量已经变成了一个LiveData对象,任何LiveData对象都可以调用它的observe()方法来观察数据的变化
observe()方法接收两个参数:第一个参数是一个LifecycleOwner对象,Activity本身就是一个LifecycleOwner对象,因此直接传this就好;第二个参数是一个Observer接口,当counter中包含的数据发生变化时,就会回调到这里,因此在这里将最新的计数更新到界面上即可

重新运行一下程序,计数器功能同样是可以正常工作的。不同的是,现在的代码更科学,也更合理,而且不用担心ViewModel的内部会不会开启线程执行耗时逻辑
不过需要注意的是,如果需要在子线程中给LiveData设置数据,一定要调用postValue()方法,而不能再使用setValue()方法,否则会发生崩溃

比较安全的LiveData使用方法

以上就是LiveData的基本用法。虽说现在的写法可以正常工作,但其实这仍然不是最规范的LiveData用法,主要的问题就在于把counter这个可变的LiveData暴露给了外部。这样即使是在ViewModel的外面也是可以给counter设置数据的,从而破坏了ViewModel数据的封装性,同时也可能带来一定的风险

比较推荐的做法是,永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据
修改MainViewModel来实现这样的功能:

class MainViewModel(countReserved: Int) : ViewModel() {

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

可以看到,这里先将原来的counter变量改名为_counter变量,并给它加上private修饰符,这样_counter变量对于外部就是不可见的了
然后又新定义了一个counter变量,将它的类型声明为不可变的LiveData,并在它的get()属性方法中返回_counter变量
这样,当外部调用counter变量时,实际上获得的就是_counter的实例,但是无法给counter设置数据,从而保证了ViewModel的数据封装性

map和switchMap

LiveData的基本用法虽说可以满足大部分的开发需求,但是当项目变得复杂之后,可能会出现一些更加特殊的需求
LiveData为了能够应对各种不同的需求场景,提供了两种转换方法:map()和switchMap()方法

map

map()方法:这个方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换

比如说有一个User类,User中包含用户的姓名和年龄,定义如下:

data class User(var firstName: String, var lastName: String, var age: Int)

如果MainActivity中明确只会显示用户的姓名,而完全不关心用户的年龄,那么这个时候还将整个User类型的LiveData暴露给外部,就显得不那么合适了
而map()方法就是专门用于解决这种问题的,它可以将User类型的LiveData自由地转型成任意其他类型的LiveData
可以在ViewModel中创建一个相应的LiveData来包含User类型的数据

class MainViewModel(countReserved: Int) : ViewModel() {
	 private val userLiveData = MutableLiveData<User>()

	val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
		"${user.firstName} ${user.lastName}"
	}
	...
}

这里调用了Transformations的map()方法来对LiveData的数据类型进行转换
map()方法接收两个参数:第一个参数是原始的LiveData对象;第二个参数是一个转换函数,在转换函数里编写具体的转换逻辑即可
这里的逻辑也很简单,就是将User对象转换成一个只包含用户姓名的字符串
另外,还将userLiveData声明成了private,以保证数据的封装性。外部使用的时候只要观察userName这个LiveData就可以了
当userLiveData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者

switchMap

switchMap()的使用场景非常固定,但是可能比map()方法要更加常用
前面所了解的所有内容都有一个前提:LiveData对象的实例都是在ViewModel中创建的。然而在实际的项目中,不可能一直是这种理想情况,很有可能ViewModel中的某个LiveData对象是调用另外的方法获取的

下面就来模拟一下这种情况,新建一个Repository单例类,代码如下所示:

object Repository {
	 fun getUser(userId: String): LiveData<User> {
		 val liveData = MutableLiveData<User>()
		 liveData.value = User(userId, userId, 0)
		 return liveData
	 }
}

这里在Repository类中添加了一个getUser()方法,这个方法接收一个userId参数
按照正常的编程逻辑,应该根据传入的userId参数去服务器请求或者到数据库中查找相应的User对象,但是这里只是模拟示例,因此每次将传入的userId当作用户姓名来创建一个新的User对象即可
需要注意的是,getUser()方法返回的是一个包含User数据的LiveData对象,而且每次调用getUser()方法都会返回一个新的LiveData实例

然后在MainViewModel中也定义一个getUser()方法,并且让它调用Repository的getUser()方法来获取LiveData对象:

class MainViewModel(countReserved: Int) : ViewModel() {
	 ...
	 fun getUser(userId: String): LiveData<User> {
	 	return Repository.getUser(userId)
	 }
}

因为每次调用getUser()方法返回的都是一个新的LiveData实例,而使用前面调用viewModel.counter的observe()方法来观察数据变化的写法,会一直观察老的LiveData实例,从而根本无法观察到数据的变化
这个时候,switchMap()方法就可以派上用场了
正如前面所说,它的使用场景非常固定:如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么就可以借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象

修改MainViewModel中的代码,如下所示:

class MainViewModel(countReserved: Int) : ViewModel() {
	 ...
	 private val userIdLiveData = MutableLiveData<String>()
	 
	 val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
	 	Repository.getUser(userId)
	 }
	 
	 fun getUser(userId: String) {
	 	userIdLiveData.value = userId
	 }
}

这里定义了一个新的userIdLiveData对象,用来观察userId的数据变化,然后调用了Transformations的switchMap()方法,用来对另一个可观察的LiveData对象进行转换
switchMap()方法同样接收两个参数:第一个参数传入新增的userIdLiveData,switchMap()方法会对它进行观察;第二个参数是一个转换函数,注意,必须在这个转换函数中返回一个LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象
那么很显然,只需要在转换函数中调用Repository的getUser()方法来得到LiveData对象,并将它返回就可以

再来梳理一遍它的整体工作流程

  • 当外部调用MainViewModel的getUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中
  • 一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数
  • 然后在转换函数中调用Repository.getUser()方法获取真正的用户数据
  • 同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象,对于Activity而言,只要去观察这个LiveData对象就可以了

下面来测试一下,修改activity_main.xml文件,在里面新增一个“Get User”按钮
然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {
	 ...
	 override fun onCreate(savedInstanceState: Bundle?) {
		 ...
		 getUserBtn.setOnClickListener {
			 val userId = (0..10000).random().toString()
			 getUser(userId)
		 }
		 user.observe(this) { user ->
            activityMainBinding.infoText.text = user.firstName
        }
	 }
	 ...
}

在“Get User”按钮的点击事件中使用随机函数生成了一个userId,然后调用MainViewModel的getUser()方法来获取用户数据,但是这个方法现在不会有任何返回值了
等数据获取完成之后,可观察LiveData对象的observe()方法将会得到通知,在这里将获取的用户名显示到界面上

运行程序,并一直点击“Get User”按钮,会发现界面上的数字会一直在变。这是因为我们传入的userId值是随机的,同时也说明switchMap()方法确实已经正常工作了

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/629366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据Doris(三十七):Spark Load导入HDFS数据

文章目录 Spark Load导入HDFS数据 一、准备HDFS数据 二、创建Doris表 三、创建Spark Load导入任务

C语言编程—常量

常量是固定值&#xff0c;在程序执行期间不会改变。这些固定的值&#xff0c;又叫做字面量。 常量可以是任何的基本数据类型&#xff0c;比如整数常量、浮点常量、字符常量&#xff0c;或字符串字面值&#xff0c;也有枚举常量。 常量就像是常规的变量&#xff0c;只不过常量…

云原生系列一:Aeraki --- 管理 Istio 服务网格中任何 7 层协议

导语:Aeraki Mesh 是 CNCF 的沙箱项目,它可以帮助你在服务网格中管理任何七层协议。 今天由叶秋学长来介绍如何通过 Aeraki 来在服务网格中为 Dubbo、Thrift 等协议的服务提供七层流量路由、本地限流、全局限流,以及如何基于 Aeraki Protocol快速开发一个自定义协议,并在 I…

CKA 06_Kubernetes 工作负载与调度 Pod 管理 yaml 资源清单 标签 Pod 生命周期 容器探针

工作负载与调度 1. Pod 管理1.1 kubectl 命令 2. yaml 资源清单2.1 yaml 文件的格式2.2 编写 yaml 资源清单 3. 标签3.1 节点标签选择器 考试题目&#xff1a;pod 中运行 nginx 和 memcache 容器4. Pod 生命周期4.1 Init 容器 5. 容器探针5.1 探测类型5.2 配置存活、就绪和启动…

驱动进化之路:总线设备驱动模型

了解总线设备驱动模型之前&#xff0c;可以先了解常规驱动程序的编写&#xff1a; LED驱动程序框架 驱动设计的思想&#xff1a;面向对象/分层/分离&#xff08;以LED操作为例&#xff09; 此次总线设备驱动模型程序的编写基于上述两种框架。 1. 总线设备驱动模型框架 在led_d…

STM32开发——串口通讯(非中断+中断)

目录 1.串口简介 2.非中断接收发送字符 3.中断接收字符 1.串口简介 通过中断的方法接受串口工具发送的字符串&#xff0c;并将其发送回串口工具。 串口发送/接收函数&#xff1a; HAL_UART_Transmit(); 串口发送数据&#xff0c;使用超时管理机制HAL_UART_Receive(); 串口…

虚拟机中Ubuntu不知root密码时设置唯一的非root用户为sudo管理员

刚才在虚拟机中玩Ubuntu的时候&#xff0c;在Cockpit管理页面把账号“服务器管理员”的选项给取消了&#xff0c;然后重新登录之后&#xff0c;就不能执行sudo指令了&#xff0c;所有需要root权限才能访问的文件&#xff08;目录&#xff09;、执行的命令全部都不行了。 执行s…

数据传输中的数据转换与处理的常用方法-物联网开发-单片机通信

目录 一、前言 二、实践与代码 1.Unsigned Char 2.memset 3.sprintf 4.atoi 5.atof 6.strcmp 7.strtok 8.strlen 9.strcpy 10.strcat 三、总结 一、前言 本文将以STM32单片机为基础&#xff0c;使用Keil5环境展示以下方法。 在单片机通信、载波通信中&#xff0c;常常涉及数…

【Laravel 6】安装需要什么环境?又怎么安装呢

服务器要求 服务器满足以下要求&#xff1a; PHP > 7.2.5BCMath PHP 拓展Ctype PHP 拓展JSON PHP 拓展Mbstring PHP 拓展OpenSSL PHP 拓展PDO PHP 拓展Tokenizer PHP 拓展XML PHP 拓展 安装Laravel Laravel 使用 Composer 来管理项目依赖。因此&#xff0c;在使用 Larav…

JDK动态代理和cglib代理

文章目录 前言1.JDK动态代理1.1 定义一个接口1.2 实现接口1.3 自定义MyInvocationHandler去实现InvocationHandler接口1.4 测试jdk代理1.5 输出代理类 2.cglib代理2.1 代理接口类2.2 代理普通类2.3 设置属性生成cglib代理类 前言 动态代理在平时的开发中用的也很多&#xff0c…

算法与数据结构(五)

一、c二叉树结构 typedef struct BiNode {int data; //结点内值域struct BiNode* lchild, * rchild; //左孩子 右孩子 } BiNode, * BiTree;或者&#xff1a; class Node { public:char data;// data表示该节点代表的内容&#xff1b;Node* L;//左子树Node* R;//右子树 }; No…

深度学习之卷积神经网络识别图片验证码实战案例(十)

案例背景&#xff1a;程序自动化的爬虫而无需人工介入是我们的最终目标。自动化爬虫避免不了自动登录的问题&#xff0c;在爬取XX数据的过程中&#xff0c;遇到登录图形验证码的识别的问题&#xff0c;那我们该如何攻破这种验证码呢&#xff1f; 字符验证码图片如下&#xff1a…

facenet, dlib人脸识别,人体检测,云数据库mysql,QQ邮箱,手机验证码,语音播报

目录 部分代码展示&#xff1a; 录入部分 识别部分​编辑 活体检测部分​编辑 同步到云数据库MySQL 其他操作 部分图片展示&#xff1a; 完整代码加ui链接&#xff1a; 涉及到的一些知识点的文章 部分代码展示&#xff1a; 录入部分 识别部分 活体检测部分 同步到云数…

峰终定律原理

峰终定律 峰终定律&#xff08; Peak–End Rule&#xff09;&#xff0c;是由丹尼尔卡尼曼&#xff08;2002年诺贝尔经济学奖获得者&#xff0c;心理学家&#xff09;提出的。 模型介绍 峰终定律是指如果在一段体验的高峰和结尾&#xff0c;体验是愉悦的&#xff0c;那么对整个…

走进机器学习

作者简介&#xff1a;本人是一名大二学生&#xff0c;就读于人工智能专业&#xff0c;学习过c&#xff0c;c&#xff0c;java&#xff0c;python&#xff0c;Mysql等编程知识&#xff0c;现在致力于学习人工智能方面的知识&#xff0c;感谢CSDN让我们相遇&#xff0c;我也会致力…

javaScript蓝桥杯---传送门

目录 一、介绍二、准备三、目标四、代码五、知识点六、完成 一、介绍 日常浏览网页的时候&#xff0c;我们会发现一个问题&#xff0c;当页面太长、内容太多的时候我们很难快速浏览到心仪的内容。为了解决这个烦恼&#xff0c;优秀的产品研发团队发明了一种类似传送门的功能&a…

对比分析:黑盒测试 VS 白盒测试

一、引言 在软件开发过程中&#xff0c;测试是确保产品质量的关键环节。其中&#xff0c;黑盒测试和白盒测试是两种常见的测试方法。本文将详细解析这两种测试方法的定义、特点&#xff0c;同时通过具体示例进行对比分析。 二、黑盒测试 黑盒测试&#xff0c;又称功能测试&…

2023最新性能测试面试题合集含答案,看完拿个20Koffer不是问题

1、描述一下你们公司的性能测试流程&#xff1f; 1&#xff09;分析性能需求&#xff08;用户使用最频繁的场景进行测试&#xff09;确定性能指标&#xff08;例如&#xff1a;事务通过率100%&#xff0c;top99%是5秒&#xff0c;最大并发是2000&#xff0c;CPU和内存都是70%以…

Git教程笔记

概念 Git是一个分布式版本控制工具&#xff0c;主要用于管理开发过程中的源代码文件&#xff08;Java类、xml文件、html页面等&#xff09;在软件开发过程中被广泛使用。 Git常用命令 Git全局设置 获取Git仓库 工作区、暂存区、版本库 概念 Git工作区中文件的状态 工作区中…

ROS EKF 机器人位姿估计功能包:robot_pose_ekf | 仿真环境实践

ROS EKF 机器人位姿估计功能包&#xff1a;robot_pose_ekf | 仿真环境实践 在仿真下使用robot_pose_ekf 在仿真下使用robot_pose_ekf 仿真环境为 一个无人机&#xff0c;具备3D POSE里程计数据&#xff0c;和imu数据。 将robot_pose_ekf.launch文件进行如下更改 <launc…