之前编写的那个计数器虽然功能非常简单,但其实是存在问题的。目前的逻辑是,当每次点击“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用法,主要的问题就在于把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的数据封装性
LiveData的基本用法虽说可以满足大部分的开发需求,但是当项目变得复杂之后,可能会出现一些更加特殊的需求
LiveData为了能够应对各种不同的需求场景,提供了两种转换方法:map()和switchMap()方法
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()的使用场景非常固定,但是可能比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对象,并将它返回就可以
再来梳理一遍它的整体工作流程
下面来测试一下,修改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()方法确实已经正常工作了