再谈协程之viewmodel-livedata难兄难弟

1,215 阅读9分钟

前言

ViewModel和LiveData最早是Google提出的AAC架构中的重要成员,那么它为什么又和协程扯上关系了呢?

其实不能叫扯上关系吧,ViewModel和LiveData属于「架构组件」,而协程是「异步工具类」,ViewModel和LiveData搭上了协程这条快车道,让Google推了几年的AAC架构更加快的让人接受了,真香。

国际惯例,官网镇楼。

developer.android.com/topic/libra…

developer.android.com/topic/libra…

这两哥们可谓是形影不离,网上的很多文章,几乎也都会同时提到它们,但是...当协程的Flow稳定之后,这两个好兄弟就突然出现了隔阂,当然,其实隔阂绝不是一天就有的,这也许是压死LiveData的最后一根稻草,Google开发者的一篇公众号,就成了这跟稻草——从LiveData迁移到Kotlin数据流。

如果你没有怎么接触Flow,那么看完这篇文章,你可能也会对LiveData鸣不平,确实,Flow提供了类似RxJava强大的异步数据流处理能力,注意,这里说的是「异步数据流」,什么是异步数据流?比如你一个界面数据由多个接口串联、并联组合起来,或者经过多次变换,再或者需要不断更新,这样的需求才是「异步数据流」,而平时大部分的业务开发,都是一个接口完事,所以,这样的需求使用Flow,就有点大材小用了,当然,Flow依然足够简单,以至于你大材小用,问题也不大,但是你不能说LiveData就完全没用了,毕竟LiveData相当单纯,单纯到它自始至终就干好了一件事,所以,并没有什么太大的必要将现有的所有LiveData都替换成Flow,而只需要在异步数据流的场景下进行替换即可。

由此可以,LiveData依然是ViewModel的好兄弟,即使这个好兄弟有着这样那样的问题。

LiveData的主要问题:

  • postValue在异步线程可能丢失数据:源码中新建Runnable的时候,只对mPendingData进行了修改,并不是加入线程池,导致数据丢失
  • 对数据流的处理能力偏弱:只提供了map、switchMap等有限的处理能力
  • 粘性事件问题:LiveData在注册时,会触发LifecycleOwner的activeStateChanged,触发了onChange,导致在注册前的事件也被发送出来

优势:

  • 简单,用于一次性请求数据简单快捷

粘滞事件:发送消息事件早于注册事件,依然能够接收到消息的为粘滞事件

简单,是LiveData还在业务场景下大范围使用的重要原因(还保留给Java代码使用也是一部分原因,毕竟协程没法在Java中使用)。

后语

在确定了学习LiveData并不是无用功之后,我们来看下如何在实际场景下利用这两兄弟来提高我们的开发效率。

我们在开发的时候,通常会在Activity中发起请求,获取网络数据,然后在回调中渲染UI数据,这是一个比较标准的渲染流程,在这个原始的流程上,我们借助ViewModel,将数据与UI隔离,同时解决了数据生命周期的问题,让数据和Activity的创建、销毁同步,中间的生命周期,不会导致数据丢失。

但这样还不够,当我们在ViewModel中请求数据后,需要回调给Activity进行UI渲染,这里还需要一个观察者的角色,当数据准备好之后,回调给Activity来执行后续的操作,这就是LiveData的作用,它是连接ViewModel和Activity的桥梁,负责了数据的传递,所以,ViewModel和LiveData,完整了一个Activity的数据传递和数据生命周期的管理,将异步数据的请求流程,更加具体和模块化了。

由此可见,LiveData作为一个数据观察者的实现,完全是可以脱离ViewModel单独在Activity中使用的,但是,这样做与直接使用RxJava之类的异步框架并没有太大区别,Google这套AAC架构的推荐方式就是:

  • Activity中获取ViewModel
  • ViewModel中通过LiveData管理数据
  • Activity中通过ViewModel获取LiveData订阅数据

这种方式的好处就是比RxJava轻量,而且将数据和UI分离,便于单元测试,不像MVP那样臃肿的同时,也更难体现分层架构的独立职责。

在这几个流程中,关于生命周期的控制,是AAC架构的一大亮点,众所周知,RxJava的内存泄漏问题,会让代码变得更加复杂,但ViewModel和LiveData,依附于Lifecycle,可以完整的在Activity和Fragment等LifecycleOwner中获取到正确的状态,从而避免了各种内存泄漏问题,而且可以封装到代码无感知,业务使用者完全不需要处理生命周期就可以避免大部分的泄漏,在简化代码的同时,也提高了性能。

LiveData能避免内存泄漏的根本原因是它与Lifecycles绑定,在非活跃状态时移除观察者,而Activity和Fragment都是LifecycleOwner,所以在Activity和Fragment中,不用对LiveData进行销毁。

ViewModel指南

ViewModel是Activity这些视图层的数据容器,我们先抛开网络请求,来看下如何在Activity中使用ViewModel。

class DataViewModel : ViewModel()

class TestActivity : AppCompatActivity() {

    val viewModel: DataViewModel = ViewModelProvider(this).get(DataViewModel::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
}

好像挺麻烦的,要通过ViewModelProvider来反射对应的类型,从而获取相应的ViewModel,这是早期的写法,也是基础,号称消灭模板代码的Kotlin,肯定是不允许这样的代码产生的。

借助委托,我们可以很方便的去除这类getXXX的代码,在Ktx中,提供了下面的委托来获取ViewModel,代码如下所示。

val viewModel by viewModels<DataViewModel>()

这也是官方推荐的初始化方式。

但这样创建的ViewModel有个小问题,我们可以看下它的源码,在ViewModelProvider中,它默认的NewInstanceFactory是使用反射来创建VIewModel的无参构造函数的,如下所示。

image-20210909172649839

但这种情况下,只适合不带参数的ViewModel,如果我们的ViewModel初始化需要传入参数呢?例如下面这样的。

class DataViewModel(val id: Int) : ViewModel()

我们可以参考ViewModelProvider.Factory的实现,创建自定义的ViewModelProvider.Factory,代码如下所示。

class DataFactory(val id: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(Int::class.java).newInstance(id) as T
    }
}

在create函数中,通过getConstructor和newInstance函数反射调用带参数的构造函数,返回ViewModel的实例。

使用的时候,viewModels的委托已经给出了自定义Factory的入口。

image-20210909174257009

代码如下,我们只需要给默认为null的factorProducer设置为我们自定义的Factory即可。

val viewModel by viewModels<DataViewModel> { DataFactory(1) }

但是,这里还需要反射吗?我直接可以拿到DataModel的实例啊,所以,自定义Factory之后,就不需要进行反射来获取实例了。

不过这样还是要写Factory,有点麻烦,所以我们进一步通过拓展函数优化下。

class ParamViewModelFactory<VM : ViewModel>(
    private val factory: () -> VM,
) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = factory() as T
}

inline fun <reified VM : ViewModel> AppCompatActivity.viewModel(
    noinline factory: () -> VM,
): Lazy<VM> = viewModels { ParamViewModelFactory(factory) }

我们直接创建ViewModel的实例来使用,参考系统ComponentActivity的viewModels拓展,创建一个自定义的viewModel拓展函数,将自定义Factory实现的代码传递进来即可。

val viewModel by viewModel { DataViewModel(1) }

LiveData指北

看了ViewModel的使用之后,我们来看下LiveData怎么来打配合。

前面我们说了,要在ViewModel中准备好UI层所需要的数据,也就是要在ViewModel中请求数据,再通过LiveData回调给UI层。LiveData为此提供了两个版本的实例——可变的和不可变的(MutableLiveData和LiveData),用来实现访问性控制。

除此之外,为了利用协程的结构化并发,ViewModel提供了viewModelScope来作为默认的可控生命周期的协程作用域,所以,我们通常会抽象出一个ViewModel基类,封装viewModelScope的调用,代码如下所示。

abstract class BaseViewModel : ViewModel() {
    /**
     * 在主线程中执行一个协程
     */
    protected fun launchOnMain(block: suspend CoroutineScope.() -> Unit): Job {
        return viewModelScope.launch(Dispatchers.Main) { block() }
    }

    /**
     * 在IO线程中执行一个协程:其实并不太需要,VM大部分时间是与UI的操作绑定,不太需要新起线程
     */
    protected fun launchOnIO(block: suspend CoroutineScope.() -> Unit): Job {
        return viewModelScope.launch(Dispatchers.IO) { block() }
    }
}

下面来看如何把数据设置给LiveData。

class DataViewModel(val id: Int) : BaseViewModel() {

    private val resultInternal = MutableLiveData<String>()
    val result: LiveData<String> = resultInternal

    fun requestData(dataID: Int): LiveData<String> {
        launchOnMain {
            val response = RetrofitClient.getXXX.getXXX(1)
            if (response.isSuccess) {
                resultInternal.value = response.data.toString()
            }
        }
        return result
    }
}

使用步骤如下:

  1. 创建一个ViewModel私有的MutableLiveData(MLD)
  2. 暴露一个不可变的LiveData
  3. 启动协程,然后将其操作结果赋给MLD

UI层使用:

class TestActivity : AppCompatActivity() {

    val viewModel by viewModel { DataViewModel(1) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.apply {
            requestData(1).observe(this@TestActivity, Observer { textView.text = it })
        }
    }
}

这有问题吗?

没有问题,就是有点麻烦不是吗?

和ViewModel一样,Kotlin当然也不允许这样的模板代码出现,所以,借助Ktx,我们同样来对其进行下简化,首先,需要引入全家桶的另一个原味鸡:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

这样就可以使用LiveData的协程构造器(coroutine builder),代码如下所示。

class DataViewModel(val id: Int) : BaseViewModel() {

    val result = liveData {
        val response = RetrofitClient.getXXX.getXXX(1)
        if (response.isSuccess) {
            emit(response.data.toString())
        }
    }
}

这个LiveData的协程构造器提供了一个协程代码块,这就是LiveData的协程作用域,当LiveData被注册的时候,作用域中的代码就会被执行,而当LiveData不再被使用时,里面的操作就会因为结构化并发而取消。而且该协程构造器返回的是一个不可变的LiveData,可以直接暴露给对应的UI层使用,在作用域中,可以通过emit()函数来更新LiveData的数据。

这样整体流程就通了,而且,非常简单不是吗?

兄弟齐心 其利断金

下面来看一个完整的例子。

class MainActivity : AppCompatActivity() {

    private val viewModel by viewModel { ViewModelLayer(10086) }
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        viewModel.result.observe(this, { binding.test.text = it.toString() })
    }
}

data class DataModel(val code: Int, val message: String = "") {
    override fun toString(): String = "Data----$code Msg----$message"
}

object RepositoryLayer {
    suspend fun getSomeData(id: Int): DataModel = withContext(Dispatchers.IO) {
        delay(2000)
        DataModel(200, "Result$id")
    }
}

class ViewModelLayer(private val id: Int) : ViewModel() {
    val result = liveData {
        try {
            emit(DataModel(0, "!!Loading!!"))
            emit(RepositoryLayer.getSomeData(id))
        } catch (e: Exception) {
            emit(DataModel(-1, "error"))
        }
    }
}

短短几行代码,我们就把ViewBinding,ViewModel,LiveData,协程,异常捕获,生命周期控制有机的融合到了一起,作为一个OneShot的UI界面,我们在极简代码的基础上,实现了良好的分层架构。

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问