重学Android Jetpack(七)之— LiveData & ViewModel

前言

这里把ViewModelLiveData放在一起来讲是因为这两者关联性比较强,当然LiveData也可以跟很多其他组件使用,就比如说我们前面介绍的Room组件。而ViewModelLiveData两者又和MVVM框架又很大的相关性,这里我们也要复习一下MVVM框架。

MVVM框架介绍

因为ViewModelLiveDataMVVM框架中的重要角色,所以我们先来复习一下MVVM框架。

MVVM框架的定义

MVVM框架是MVC框架的加强版,和MVC相比,MVVM多了一个VM,也就是ViewModelMVVM框架主要解决的是MVC框架Controller中数据和业务逻辑耦合的问题,所以ViewModel其实是ModelController中间交互的媒介。

MVVM架构结构图:

MVVM.png

从结构图我们可以看出MVVM的主要三点是:

  • View: 对应ActivityXML,负责View的绘制以及使用交互;
  • Model: 数据实体模型;
  • ViewModel: 负责完成ViewModel间的交互,负责业务逻辑。

MVVM框架的优缺点

优点:

  • 低耦合度:数据和业务逻辑都在对应的ViewModel中,ViewModel只关注数据和业务逻辑,不用和UI耦合;
  • 可复用性高:一个ViewModel可以复用到多个View中,对于UI的单元测试会更方便;
  • 开发解耦:业务逻辑和UI界面开发可以同时分开给不同的开发者做,两者并不会有太大的耦合。

缺点:

  • 代码上肯定会比MVC的更多,因为在ViewModel之间多了一个交互的桥梁,但是会比MVP简洁,用过MVP都知道那接口多得不要不要的;
  • 如果使用类似DataBinding来进行数据和View的双向绑定会造成调试比较麻烦,View的状态根据数据改变较多,ViewModel的构建和维护的成本都会比较高。

MVVM框架使用选择总结

由上面分析可见,MVVM框架并不能带来比MVCMVP更少的代码量,但UI和业务逻辑的分离得更好,开发逻辑上会更清晰,对于在多页面且交互逻辑相对复杂的应用开发时会能更好让团队协作开发。

LiveData

简介

LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData具有生命周期感知能力,意指它遵循其他应用组件(如ActivityFragmentService的生命周期。这种感知能力可确保LiveData仅更新处于活跃生命周期状态的应用组件观察者。 如果观察者(由Observer类表示)的生命周期处于STARTEDRESUMED状态,则LiveData会认为该观察者处于活跃状态。LiveData只会将更新通知给活跃的观察者。为观察LiveData对象而注册的非活跃观察者不会收到更改通知。

LiveData和ViewModel的关系

LiveData的作用就是让数据具有生命周期感知能力,并对数据的变化进行实时监听,在Activity/Fragment等处在活跃状态时,自动把数据回调给观察者进行数据更新。而 ViewModel 的作用则是可以对LiveData进行正确的保存和恢复。所以LiveDataViewModel在整个MVVM架构中是担当一个数据驱动的职责。

使用LiveData观察数据的优势

  • 数据变更时自动通知UI更新;
  • 可避免内存泄漏,这是因为观察者都绑定到Lifecycle上;
  • 避免Activity/Fragment因销毁而操作的崩溃,因为处于不活跃的界面不会接收到任何LiveData事件;
  • 不再需要手动处理生命周期,因为LiveData观察数据的操作是可以感知相关的生命周期状态变化的;
  • 数据总是会处于最新的状态,Lifecycle主要从非活跃状态到活跃状态变化就会更新数据;
  • 当系统配置变化时会对数据进行保存和恢复,及UI状态的恢复,如屏幕旋转后的恢复;
  • 资源共享,通过单例来控制LiveData,这样可以把当前LiveData的事件发给所有观察者。

LiveData的使用

LiveData最佳的实践方法就是和ViewModel一起配合使用,因为它们两者的功能是具有互补性的,但是LiveData也可以单独使用,下面LiveData使用的介绍我们先用单独使用来说明先,后面学习ViewModel再介绍两者结合使用的案例。

依赖LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-beta01"
复制代码
LiveData使用三步骤
  • 创建LiveData实例以存储某种类型的数据,可以是基本类型的String,也可以是自定义的数据实体UserModel,一般情况下这些操作都在ViewModel中完成。
  • 创建可定义onChanged()方法的Observer对象,该方法可以控制当LiveData对象存储的数据更改时会发生什么。通常情况下,您可以在界面控制器(如ActivityFragment)中创建Observer对象。
  • 使用observe()方法将 Observer 对象附加到LiveData对象。observe()方法会采用LifecycleOwner对象。这样会使 Observer 对象订阅LiveData对象,以使其收到有关更改的通知。通常情况下,您可以在界面控制器(如ActivityFragment)中附加Observer对象。

创建一个数据实体:

data class UserModel(
    var name: String,
    var age: Int
)
复制代码

创建一个LiveData存储UserModel类型数据:

private var mutableLiveData = MutableLiveData<UserModel>()
复制代码

模拟一个加载数据赋值的过程:

private fun loadUserInfo(){
    rlLoading?.visibility = View.VISIBLE
    Handler(Looper.getMainLooper()).postDelayed(Runnable {
        val userModel: UserModel = if(count % 2 == 0){
            UserModel("张三",24)
        }else{
            UserModel("李美美",24)
        }
        count++
        mutableLiveData.postValue(userModel)
        rlLoading?.visibility = View.GONE
    },2000)
}
复制代码

最后是通过LiveDataobserve(...)方法把this传进去,因为AppCompatActivity默认是实现了Lifecycle的,这个这个this就是lifecycleOwner,那么就绑定了观察者啦:

mutableLiveData.observe(this, object :Observer<UserModel>{
    override fun onChanged(it: UserModel?) {
        it?.let {
            tvName?.text = it.name
            tvAge?.text = "${it.age} 岁"
        }
    }
})
复制代码

通过button点击调用:

btnLoad?.setOnClickListener {
    loadUserInfo()
}
复制代码

效果如下:

1118.gif

LiveData和Room结合使用

我们拿之前介绍Room组件的demo来改一下实现LiveDataRoom结合使用,文章链接:juejin.cn/post/709022…

首先改一下接口WordDao,把查询数据的方法返回改成LiveData类型:

//获取所有数据
@Query("SELECT * FROM word_table")
fun queryAll(): LiveData<MutableList<WordEntity>>
复制代码

如果你要对一个数据这样观察也可以改成这样。然后在Activity中的监听使用:

val liveDataWord = wordDao.queryAll()
liveDataWord.observe(this,object :Observer<MutableList<WordEntity>>{
    override fun onChanged(t: MutableList<WordEntity>?) {
        if (t != null) {
            mList.clear()
            mList.addAll(t)
        }
        wordAdapter.notifyDataSetChanged()
    }
})
复制代码

将协程与 LiveData 一起使用

当使用LiveData时进行异步任务是,我们可以使用liveData构建器函数调用 suspend 函数,并将结果作为LiveData对象传送。这个liveData的功能其实跟我们之前学习Kotlinflow很类似,但是flow会优于liveData,所以谷歌官方才会建议使用flow代替liveData,当然flowKotlin语言特性,使用Java的时候就比较尴尬了,所以liveData还是有很大的存在意义,而且它也满足一般项目的开发情况。

声明一个LiveData对象:

private var liveDataUser: LiveData<UserModel>? = null
复制代码

调用:

liveDataUser = liveData {
    delay(2000)
    val userModel = UserModel("张三",23)
    emit(userModel)
}

liveDataUser?.observe(this,object :Observer<UserModel>{
    override fun onChanged(t: UserModel?) {
        Log.e("liveDataUser",t.toString())
    }
})
复制代码

我们也可以写成这样:

liveData {
    delay(2000)
    val userModel = UserModel("张三",23)
    emit(userModel)
}.observe(this,object :Observer<UserModel>{
    override fun onChanged(t: UserModel?) {
        Log.e("liveDataUser",t.toString())
    }
})
复制代码

扩展LiveData

如果观察者的生命周期处于STARTED或 RESUMED状态,则LiveData会认为该观察者处于活跃状态,我们需要在观察者的lifecycle处于active状态做处理时需要继承LiveDataMutableLiveData并覆写它的onActive()onInactive()方法。

下面我们完善一下官方举的案例来说明一下LiveData扩展,案例介绍的是监听股票价格变化,在界面活跃的时候监听,不活跃的时候移除监听,减少不必要的开支。

LiveData包裹的数据实体:

data class StockInfo(
    val price :String,
    val gain :String
)
复制代码

扩展的LiveData类中监听数据的接口:

interface StockInfoListener {

    fun sendStockInfo(stockInfo: StockInfo)
}
复制代码

数据管理类:StockManager

object StockManager {
    private var stockInfoListener: StockInfoListener? = null
    private var flag: Boolean = false
    private var gain: Int = 0

    fun stockInfoUpdate(stockInfoListener: StockInfoListener?) {
        this.stockInfoListener = stockInfoListener
        flag = true
        updateStockInfo()
    }

    fun removeUpdates() {
        flag = false
        this.stockInfoListener = null
    }

    private fun updateStockInfo() {
        Thread {
            while (flag) {
                Thread.sleep(2000)
                gain++
                stockInfoListener?.sendStockInfo(StockInfo("20.$gain", "$gain%"))
            }
        }.start()

    }
}
复制代码

这里提供添加监听的方法,添加监听后就更新数据,这里模拟的是每两秒更新一次,那对于真实开发环境就是请求后台的接口得到数据在发送回LiveData扩展类里面。

LiveData扩展类:

class StockLiveData: LiveData<StockInfo>() {

    private val TAG = StockLiveData::class.java.simpleName

    private val stockInfoListener = object : StockInfoListener {
        override fun sendStockInfo(stockInfo: StockInfo) {
            Log.i(TAG,stockInfo.toString())
            postValue(stockInfo)
        }
    }

    override fun onActive() {
        super.onActive()
        Log.i(TAG,"onActive")
        StockManager.stockInfoUpdate(stockInfoListener)
    }


    override fun onInactive() {
        super.onInactive()
        StockManager.removeUpdates()
        Log.i(TAG,"onInactive")
    }

    companion object{
        private var instance: StockLiveData? = null
        @MainThread
        fun get(): StockLiveData?{
            if(instance == null){
                synchronized(StockLiveData::class.java){
                    if(instance == null){
                        instance = StockLiveData()
                    }
                }
            }
            return instance
        }
    }
}
复制代码

Activity中对StockLiveData的观察监听:

StockLiveData.get()?.observe(this, object : Observer<StockInfo> {
    @SuppressLint("SetTextI18n")
    override fun onChanged(t: StockInfo?) {
        Log.i("MainActivity",t.toString())
        tvPrice?.text = "限价:${t?.price}"
        tvGain?.text = "涨幅:${t?.gain}"
    }
})
复制代码

效果图如下:

123.gif

LiveData转换

LiveData在给观察者发送数据之前要对存储在其中的值进行更改,或者可能需要根据另一个实例的值返回不同的LiveData实例时,我们需要使用Lifecycle软件包提供的Transformations类进行LiveData转换。

Transformations.map()

该方法主要是对存储在LiveData对象中的值通过方法转换成其他类型,并将结果传播到下游,例如在实际开发中数据实体放回给一个用户的年龄age是Int类型,所以我们不能直接使用TextView设置它,并且我们还要加上数字年龄后面加个字,如果有多个地方都要显示,那我们每次都要拼接一下,用Transformations.map()LiveData转换只需要一次即可。看下面代码:

private val userLiveData = MutableLiveData<UserModel>()

private fun getUserLiveData() {
    Handler(Looper.getMainLooper()).postDelayed({
        val userModel = UserModel("张三", 24)
        userLiveData.postValue(userModel)
    }, 2000)
}
复制代码

声明一个UserModelLiveData,并模拟获取UserLiveData的方法。接着在Activity中对LiveData监听:

val userNameLiveData: LiveData<String> = Transformations.map(userLiveData) {
    "${it.age} 岁"
}

userNameLiveData.observe(this, Observer {
    tvAge?.text = it
})

getUserLiveData()
复制代码

也可以连着写:

Transformations.map(userLiveData) {
    "${it.age} 岁"
}.observe(this, Observer {
    tvAge?.text = it
})
复制代码

Transformations.switchMap()

Transformations.switchMap()方法和Transformations.map()方法类似,不同的是Transformations.switchMap()方法是必须返回一个LiveData对象。在实际开发中的情况就是我们有时候既要维护用户userIdLiveData,又要维护userId对应UserModelLiveData对象,这个时候就可以通过Transformations.switchMap()方法根据userIdLiveData来获取到UserModel。上面我们使用的UserModel增加一个参数id,我们通过这个id去查找对应的User。看下面代码:

data class UserModel(
    var id: Int,
    var name: String,
    var age: Int
)
复制代码

制造一些数据:

private val userList = ArrayList<UserModel>()

userList.add(UserModel(123,"张三", 23))
userList.add(UserModel(129,"李四", 25))
userList.add(UserModel(132,"王五", 27))
复制代码

设置一个返回LiveData<UserModel>的方法:

private fun getUserLiveData(id: Int): LiveData<UserModel>{
    var userModel: UserModel? = null
    for (e in userList){
        if(id == e.id){
            userModel = e
        }
    }
    return MutableLiveData(userModel)
}
复制代码

设置观察监听:

val idLiveData: LiveData<Int> = MutableLiveData(123)
Transformations.switchMap(idLiveData){
   getUserLiveData(it)
}.observe(this,object: Observer<UserModel>{
    override fun onChanged(t: UserModel?) {
        Log.i("MainActivity",t.toString())
    }
})
复制代码

合并多个LiveData数据源

合并LiveData数据源使用的MediatorLiveData,它继承自mutableLiveData,可将多个LiveData数据源集合起来,可以达到一个组件监听多个LiveData数据变化的目的。在实际开发场景中,当我们要本地数据库或网络更新的LiveData对象时就可以用MediatorLiveData对两个LiveData源进行合并。看下面代码:

val mutableLiveData1 = MutableLiveData<String>()
val mutableLiveData2 = MutableLiveData<String>()
val liveDataManger = MediatorLiveData<String>()

liveDataManger.addSource(mutableLiveData1) {
    Log.i(TAG,"mutableLiveData1 -> $it")
    liveDataManger.value = it
}

liveDataManger.addSource(mutableLiveData2) {
    Log.i(TAG,"mutableLiveData2 -> $it")
    liveDataManger.value = it
}

liveDataManger.observe(this) {
    Log.i(TAG,"liveDataMerger -> $it")
}

mutableLiveData2.postValue("合并LiveData数据源")
复制代码

打印结果: com.qisan.livedata I/MainActivity: mutableLiveData2 -> 合并LiveData数据源 com.qisan.livedata I/MainActivity: liveDataMerger -> 合并LiveData数据源

ViewModel

ViewModel是什么

ViewModel是一个注重生命周期的方式存储和管理界面相关数据的类,而且它和配合LiveData可以让数据在发生屏幕旋转等配置更改后继续留存。

Android框架可以管理界面控制器(如Activity Fragment)的生命周期。Android 框架可能会决定销毁或重新创建界面控制器,以响应完全不受您控制的某些用户操作或设备事件。如果系统销毁或重新创建界面控制器,则存储在其中的任何瞬态界面相关数据都会丢失。例如,应用可能会在它的某个 Activity 中包含用户列表。为配置更改重新创建 Activity 后,新 Activity 必须重新提取用户列表。对于简单的数据,Activity可以使用onSaveInstanceState()方法从onCreate()中的捆绑包恢复其数据,但此方法仅适合可以序列化再反序列化的少量数据,而不适合数量可能较大的数据,如用户列表或位图。

ViewModel和LiveData的结合使用

创建ViewModel

class UserViewModel: ViewModel() {

    private val userModel: MutableLiveData<UserModel> by lazy {
        MutableLiveData<UserModel>().also {
            it.postValue(loadUser())
        }
    }

    fun getUser(): LiveData<UserModel>{

        return userModel
    }

    fun loadUser(): UserModel{

        return UserModel(123,"小明",23)
    }
}
复制代码

这里加载数据如果是在真实的开发环境中是一个异步任务,我们可以使用协程来实现。

Activity中调用:

val userVm: UserViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
userVm.getUser().observe(this){
    tvName?.text = it.name
    tvAge?.text = "${it.age} 岁"
}
复制代码

ViewModel通过ViewModelProvider来创建,当Activity发生旋转之类的配置后进行重建时,onCreate()方法会重新走一遍,但userVm实例还是第一次创建的实例,这是因为在ViewModelProvider(this).get(UserViewModel::class.java)中的get方法中进行了缓存操作。当Activity调用finish()方法后,框架会调用ViewModel对象的onCleared()方法,以便对它进行数据的清理。ViewModel的生命周期如下图:

来自官方文档提供的结构图 1234.png

我们通常在应用首次调用Activity对象的onCreate()方法时请求ViewModel。应用可能会在activity的整个生命周期内多次调用onCreate(),如在旋转设备屏幕时。ViewModel存在的时间范围是从首次请求ViewModel直到activity完成并销毁。

在Fragment之间共享数据

Fragment之间的通信在以往我们的做法基本都是EventBus用得比较多,而ViewModel可以让我们的通信更简单。用法也很监听,一个发送数据,一个监听接受就行:

发送数据的Fragment:

class FragmentA : Fragment() {

    val tv_a_b by bindView<TextView>(R.id.tv_a_b)

    companion object {
        fun newInstance() : FragmentA{
            val bundle = Bundle()
            val fragment = FragmentA()
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_a,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userVm: UserViewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)

        tv_a_b?.setOnClickListener {
            userVm.sendMsg(UserModel(123,"李美美",23))
        }
    }
}
复制代码

接受数据的Fragment:

class FragmentB : Fragment() {

    val tv_b_a by bindView<TextView>(R.id.tv_b_a)

    companion object {
        fun newInstance() : FragmentB{
            val bundle = Bundle()
            val fragment = FragmentB()
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_b,container,false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userVm: UserViewModel = ViewModelProvider(requireActivity()).get(UserViewModel::class.java)
        userVm.getUser().observe(viewLifecycleOwner, Observer {
            
            tv_b_a?.text = "${it.name} ${it.age}岁"
        })
    }
}
复制代码

这里要注意的是ViewModelProvider(requireActivity()).get(UserViewModel::class.java)方法中要传requireActivity(),别用this或者activity,这两个Fragment都会检索包含它们的Activity。这样,当这两个Fragment各自获取ViewModelProvider时,它们会收到相同的UserViewModel实例,其范围限定为该Activity

这种数据通信方式还具有以下优势:

  • Activity不需要执行任何操作,也不需要对此通信有任何了解。
  • 除了UserViewModel约定之外,Fragment之间不需要相互了解。如果其中一个 Fragment消失,另一个Fragment将继续照常工作。
  • 每个Fragment都有自己的生命周期,而不受另一个Fragment的生命周期的影响。如果一个Fragment替换另一个Fragment,界面将继续工作而没有任何问题。

将协程与ViewModel一起使用

androidx.lifecycle系列库中为每个ViewModel定义了 ViewModelScope,在ViewModel中调用viewModelScope.launch {}实现协程的异步任务,如果ViewModel已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在ViewModel处于活动状态时才需要完成的工作,此时协程就非常适用。

class UserViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}
复制代码

总结

LiveData && ViewModel的使用介绍就讲完了,后续文章会继续分享一下目前项目中使用ViewModel时如何加入Activity的封装。谷歌推出的LiveDataViewModel是建议两者结合使用的,因为ViewModel可以在系统配置更改的情况下保存和恢复LiveData,而LiveData则可以在生命周期方法内对数据的变化进行监听。 两者的使用通过上面的介绍其实也并不复杂,但是对于代码的稳定性却是带来很大的提高,所以是时候把公司的项目都换成MVVM啦。

猜你喜欢

转载自juejin.im/post/7095629164350275592