目录
简单介绍
之前我们编写的那个计数器虽然功能非常简单,但其实是存在问题的。目前的逻辑是,当每次点击“Plus One”按钮时,都会先给ViewModel中的计数加1,然后立即获取最新的计数。这种方式在单线程模式下确实可以正常工作,但如果ViewModel的内部开启了线程去执行一些耗时逻辑,那么在点击按钮后就立即去获取最新的数据,得到的肯定还是之前的数据。
你会发现,原来我们一直使用的都是在Activity中手动获取ViewModel中的数据这种交互方式,但是ViewModel却无法将数据的变化主动通知给Activity。
或许你会说,我把Activity的实例传给ViewModel,这样ViewModel不就能主动对Activity进行通知了吗? 注意,千万不可以这么做。不要忘了,ViewModel的生命周期是长于Activity的,如果把Activity的实例传给ViewModel,就很有可能会因为Activity无法释放而造成内存泄漏,这是一种非常错误的做法。
而这个问题的解决方案也是显而易见的,就是使用我们本节即将学习的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来作为默认计数
这样我们就借助LiveData将MainViewModel的写法改造完了,接下来开始改造MainActivity,代码如下所示:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
plusOneBtn.setOnClickListener {viewModel.plusOne(){
clearBtn.setOnClickListener {viewModel.clear()
viewModel.counter.observe(this, Observer {count ->infoText.text = count.toString()
})
override fun onPause() {
super.onPause()
sp.edit{
putInt("countreserved",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用法,主要的问题就在于我们将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
是一个只读的LiveData<Int>
属性,外部类可以通过观察它来获取计数器的变化。_counter
是一个私有的MutableLiveData<Int>
变量,用于保存整数类型的数据。
可以看到,这里先将原来的counter变量改名为 _counter变量,并给它加上private修饰符,这样 counter变量对于外部就是不可见的了。然后我们又新定义了一个counter变量将它的类型声明为不可变的LiveData,并在它的get()属性方法中返回 counter变量。
这样,当外部调用counter变量时,实际上获得的就是 counter的实例,但是无法给counter设置数据,从而保证了ViewModel的数据封装性。
在 Kotlin 中,变量是对对象的引用。所以,当你声明 val counter: LiveData<Int> get() = _counter
时,counter
变量引用了 _counter
。这并不意味着创建了一个新的 LiveData
实例,而是 counter
和 _counter
实际上都引用相同的 LiveData
实例。
_counter
被声明为 MutableLiveData<Int>
,但它是私有的。只有 MainViewModel
类内部的方法(如 plusOne()
和 clear()
)可以修改 _counter
的值。外部类无法直接修改 _counter
,因此确保了对计数器数据的更改是通过 ViewModel 提供的公共方法进行的。
虽然 counter
和 _counter
引用的是相同的 LiveData
实例,但 counter
作为只读属性,限制了外部类对 _counter
的直接访问和修改。因此,这样的设计在一定程度上体现了对内部数据的封装性。
目前这种写法可以说是非常规范了,这也是Android官方最为推荐的写法。
map和switchMap
介绍
LiveData的基本用法虽说可以满足大部分的开发需求,但是当项目变得复杂之后,可能会出现一些更加特殊的需求。LiveData为了能够应对各种不同的需求场景,提供了两种转换方法:map()和switchMap()方法。下面我们就学习这两种转换方法的具体用法和使用场景
map
先来看map()方法,这个方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。那么什么情况下会用到这个方法呢 ?下面我来举一个例子。
比如说有一个User类,User中包含用户的姓名和年龄,定义如下
data class User(var firstName: String, var lastName: String, var age: Int)
我们可以在ViewModel中创建一个相应的LiveData来包含User类型的数据,如下所示:
class MainViewModel(countReserved: Int) : ViewModel() {
val userLiveData = MutableLiveData<User>()
. . .
}
到目前为止,这和我们在上一小节中学习的内容并没有什么区别。可是如果MainActivity中明确只会显示用户的姓名,而完全不关心用户的年龄,那么这个时候还将整个User类型的LiveData暴露给外部,就显得不那么合适了。
而map()方法就是专门用于解决这种问题的,它可以将User类型的LiveData自由地转型成任意其他类型的LiveData,下面我们来看一下具体的用法 :
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的观察者
这就是map()方法的用法和使用场景,非常好理解
接下来,我们开始学习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)
}
}
接下来的问题就是,在Activity中如何观察LiveData的数据变化呢 ? 既然getUser()方法返回的就是一个LiveData对象,那么我们可不可以直接在Activity中使用如下写法呢?
viewModel.getUser(userId) .observe( this) { user ->
}
请注意,这么做是完全错误的。因为每次调用getUser()方法返回的都是一个新的LiveData实例,而上述写法会一直观察老的LiveData实例,从而根本无法观察到数据的变化。你会发现这种情况下的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()方法同样接收两个参数:第一个参数传入我们新增的userIdLiveDataswitchMap()方法会对它进行观察;第二个参数是一个转换函数,注意,我们必须在这个转换函数中返回一个LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象。那么很显然,我们只需要在转换函数中调用Repository的getUser()方法来得到LiveData对象,并将它返回就可以了。
为了让你能更清晰地理解switchMap()的用法,我们再来桥理一遍它的整体工作流程。首先当外部调用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( )
viewModel.getUser(userId)
}
viewModel.user.observe( this, Observer user ->
infoText.text = user.firstName
})
...
}
具体的用法就是这样了,我们在“Get User”按钮的点击事件中使用随机函数生成了一个userId,然后调用MainViewModel的getUser()方法来获取用户数据,但是这个方法现在不会有任何返回值了。等数据获取完成之后,可观察LiveData对象的observe()方法将会得到通知,我们在这里将获取的用户名显示到界面上。
文章内容均来自郭霖的《Android第一行代码》第三版