前言
ViewModel和LiveData最早是Google提出的AAC架构中的重要成员,那么它为什么又和协程扯上关系了呢?
其实不能叫扯上关系吧,ViewModel和LiveData属于「架构组件」,而协程是「异步工具类」,ViewModel和LiveData搭上了协程这条快车道,让Google推了几年的AAC架构更加快的让人接受了,真香。
国际惯例,官网镇楼。
https://developer.android.com/topic/libraries/architecture/viewmodel
https://developer.android.com/topic/libraries/architecture/livedata
这两哥们可谓是形影不离,网上的很多文章,几乎也都会同时提到它们,但是...当协程的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。
代码语言:javascript复制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,代码如下所示。
代码语言:javascript复制val viewModel by viewModels<DataViewModel>()
这也是官方推荐的初始化方式。
但这样创建的ViewModel有个小问题,我们可以看下它的源码,在ViewModelProvider中,它默认的NewInstanceFactory是使用反射来创建VIewModel的无参构造函数的,如下所示。
image-20210909172649839
但这种情况下,只适合不带参数的ViewModel,如果我们的ViewModel初始化需要传入参数呢?例如下面这样的。
代码语言:javascript复制class DataViewModel(val id: Int) : ViewModel()
我们可以参考ViewModelProvider.Factory的实现,创建自定义的ViewModelProvider.Factory,代码如下所示。
代码语言:javascript复制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即可。
代码语言:javascript复制val viewModel by viewModels<DataViewModel> { DataFactory(1) }
但是,这里还需要反射吗?我直接可以拿到DataModel的实例啊,所以,自定义Factory之后,就不需要进行反射来获取实例了。
不过这样还是要写Factory,有点麻烦,所以我们进一步通过拓展函数优化下。
代码语言:javascript复制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实现的代码传递进来即可。
代码语言:javascript复制val viewModel by viewModel { DataViewModel(1) }
LiveData指北
看了ViewModel的使用之后,我们来看下LiveData怎么来打配合。
前面我们说了,要在ViewModel中准备好UI层所需要的数据,也就是要在ViewModel中请求数据,再通过LiveData回调给UI层。LiveData为此提供了两个版本的实例——可变的和不可变的(MutableLiveData和LiveData),用来实现访问性控制。
除此之外,为了利用协程的结构化并发,ViewModel提供了viewModelScope来作为默认的可控生命周期的协程作用域,所以,我们通常会抽象出一个ViewModel基类,封装viewModelScope的调用,代码如下所示。
代码语言:javascript复制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。
代码语言:javascript复制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
}
}
使用步骤如下:
- 创建一个ViewModel私有的MutableLiveData(MLD)
- 暴露一个不可变的LiveData
- 启动协程,然后将其操作结果赋给MLD
UI层使用:
代码语言:javascript复制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,我们同样来对其进行下简化,首先,需要引入全家桶的另一个原味鸡:
代码语言:javascript复制implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
这样就可以使用LiveData的协程构造器(coroutine builder),代码如下所示。
代码语言:javascript复制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的数据。
这样整体流程就通了,而且,非常简单不是吗?
兄弟齐心 其利断金
下面来看一个完整的例子。
代码语言:javascript复制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界面,我们在极简代码的基础上,实现了良好的分层架构。
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问