探究Jetpack(二)之LiveData


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()方法确实已经正常工作了

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值