Jatpack之LiveDate

目录

 简单介绍

基本使用

优化

map和switchMap

介绍

map


 简单介绍

       之前我们编写的那个计数器虽然功能非常简单,但其实是存在问题的。目前的逻辑是,当每次点击“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第一行代码》第三版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值