4

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

 2 years ago
source link: https://xuyisheng.top/viewmodel-livedata/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Kotlin 阅读:  11

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。

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界面,我们在极简代码的基础上,实现了良好的分层架构。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK