当前位置: 首页 > 科技观察

如何在Android上正确使用Kotlin协程?

时间:2023-03-17 20:06:30 科技观察

序还记得哪一年GoogleIO正式宣布Kotlin成为Android第一级开发语言吗?那是GoogleIO2017,两年过去了,从一个Android开发者的角度来看,Kotlin的生态环境越来越好,相关的开源项目和学习资料也越来越丰富。愿意使用或尝试Kotlin的朋友也变多了。常年在掘金,能明显感觉到打着Kotlin标签的文章在逐渐增多(其实还是少得可怜)。今年的GoogleIO也发布了KotlinFirst的口号,很多新的API和特性都会优先支持Kotlin。因此,时至今日,Android开发者真的没有理由不学习Kotlin。今天要讲的是KotlinCoroutine。虽然在Kotlin发布之初就有协程,但是直到2018年的KotlinConf大会上,JetBrain发布了Kotlin1.3RC才带来了协程的稳定版本。尽管协程的稳定版已经推出一年多了,但它似乎并没有足够的用户,至少在我看来是这样。在我学习协程的各个阶段,遇到问题可以求助的地方很少,基本都迷失在技术组。基本上只能靠一些英文文档来解决问题。看了半天官方文档,几乎只知道GlobalScope。确实,官方文档从头到尾基本都是用GlobalScope写的示例代码。所以一些开发者,包括我自己,在写自己的代码时直接使用GlobalScope。偶然间才发现,这样的问题其实很大。在Android中,一般不推荐直接使用GlobalScope。那么,如何在Android中正确使用协程呢?再细分一点,如何直接在Activity中使用呢?如何与ViewModel、LiveData、LifeCycle等一起使用?我将通过简单的示例代码来解释Android上的协程。你也可以跟着你的手一起敲击。AndroidGlobalScope上协程的使用在一般的应用场景中,我们都希望能够异步的执行一些耗时的任务,比如网络请求,数据处理等等。当我们离开当前页面时,我们也希望取消正在进行的异步任务。这两点正是你在使用协程时需要注意的。既然不建议直接使用GlobalScope,那我们试试看会有什么效果。privatefunlaunchFromGlobalScope(){GlobalScope.launch(Dispatchers.Main){valdeferred=async(Dispatchers.IO){//networkrequestdelay(3000)"Getit"}globalScope.text=deferred.await()Toast.makeText(applicationContext,"GlobalScope",Toast.LENGTH_SHORT).show()}}在launchFromGlobalScope()方法中,我直接通过GlobalScope.launch()启动一个协程,delay(3000)模拟一次网络请求。三秒后会弹出Toast提示。使用没有问题,Toast可以正常弹出。但是当你执行这个方法的时候,马上按回车键返回上一页,Toast还是会弹出来。如果在实际开发中通过网络请求更新页面,当用户已经不在这个页面上时,根本就没有必要去请求,只会浪费资源。GlobalScope显然不适合这个特性。https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html其实也很详细,如下:没有任何与之关联的工作。应用程序代码通常应该使用应用程序定义的协程作用域。强烈建议不要在GlobalScope的实例上使用同步器或启动。大致来说,Globalscope通常用于启动顶层协程,这些协程贯穿应用的整个生命周期,不会被过早取消。程序代码通常应使用自定义协程作用域。强烈建议不要直接使用GlobalScope的异步或启动方法。GlobalScope创建的协程没有父协程,并且GlobalScope通常不绑定任何生命周期组件。除非手动管理,否则很难满足我们实际开发的需要。因此,如果可以的话,尽量不要使用GlobalScope。MainScope的官方文档中提到需要使用自定义协程作用域。当然,Kotlin已经为我们提供了一个合适的协程作用域MainScope。看一下MainScope的定义:publicfunMainScope():CoroutineScope=ContextScope(SupervisorJob()+Dispatchers.Main)记住这个定义,后面在ViewModel的协程使用中会有参考作用。为我们的Activity实现自己的协程作用域:classBasicCorotineActivity:AppCompatActivity(),CoroutineScopebyMainScope(){}通过扩展函数launch(),可以直接在主线程中启动协程。示例代码如下:privatefunlaunchFromMainScope(){launch{valdeferred=async(Dispatchers.IO){//networkrequestdelay(3000)"Getit"}mainScope.text=deferred.await()Toast.makeText(applicationContext,"MainScope",Toast.LENGTH_SHORT).show()}}最后别忘了在onDestroy()中取消协程,是通过扩展函数cancel()实现的:overridefunonDestroy(){super.onDestroy()cancel()}现在测试launchFromMainScope()方法!你会发现这正是你想要的。在实际开发中,MainScope可以集成到BaseActivity中,不需要重复编写模板代码。ViewModelScope如果使用MVVM架构,根本不会在Activity上写任何逻辑代码,更不会启动协程。这时候,大部分工作都会交给ViewModel。那么如何在ViewModel中定义协程作用域呢?还记得上面MainScope()的定义吗?没错,搬过来直接用就行了。classViewModelOne:ViewModel(){privatevalviewModelJob=SupervisorJob()privatevaluiScope=CoroutineScope(Dispatchers.Main+viewModelJob)valmMessage:MutableLiveData=MutableLiveData()fungetMessage(message:String){uiScope.launch{valdeferred=async(Dispatchers.IO){delay(2000)"post$message"}mMessage.value=deferred.await()}}overridefunonCleared(){super.onCleared()viewModelJob.cancel()}}这里的uiScope其??实相当于MainScope。调用getMessage()方法与前面的launchFromMainScope()效果相同。请记住在ViewModel的onCleared()回调中取消协程。您可以定义一个BaseViewModel来处理这些逻辑,避免重复模板代码。但是,Kotlin就是让你做同样的事情,少写代码,所以viewmodel-ktx就来了。看到ktx,你应该明白是为了简化你的代码。引入如下依赖:implementation"androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03"然后什么都不用做,直接使用coroutinescopeviewModelScope即可。viewModelScope是ViewModel的一个扩展属性,定义如下:+Dispatchers.Main))}看代码应该就明白了,还是那个熟悉的。当调用ViewModel.onCleared()时,viewModelScope会自动取消范围内的所有协程。使用示例如下:fungetMessageByViewModel(){viewModelScope.launch{valdeferred=async(Dispatchers.IO){getMessage("ViewModelKtx")}mMessage.value=deferred.await()}}写到这里,viewModelScope最好满足速记的需要。其实整篇文章写下来,viewModelScope在我看来依然是最好的选择。LiveDataKotlin还赋予了LiveData直接使用协程的能力。添加如下依赖:implementation"androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03"在liveData{}代码块中直接调用需要异步执行的挂起函数,调用emit()函数即可发送处理结果。示例代码如下:valmResult:LiveData=liveData{valstring=getMessage("LiveDataKtx")emit(string)}大家可能好奇这里好像没有显示调用,liveData代码块在哪执行?什么?当LiveData进入active状态时,liveData{}会自动执行。当LiveData进入非活动状态时,它将在可配置的超时后自动取消。如果在完成之前取消,它将在LiveData再次激活时重新运行。如果上一次运行成功结束,则不会重新运行。也就是说只有自动取消的liveData{}才能重新运行。由于其他原因(例如CancellationException)而取消也不会重新运行。因此,livedata-ktx的使用受到了限制。对于需要用户主动刷新的场景,是无法满足的。在一个完整的生命周期中,一旦成功执行完成,就没有办法再次触发。不知道这句话对不对,我个人是这么理解的。因此viewmodel-ktx的适用性更广,可控性更好。LifecycleScopeimplementation"androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03"lifecycle-runtime-ktx通过扩展属性为每个LifeCycle对象定义了协程作用域lifecycleScope。您可以通过lifecycle.coroutineScope或lifecycleOwner.lifecycleScope访问它。示例代码如下:fungetMessageByLifeCycle(lifecycleOwner:LifecycleOwner){lifecycleOwner.lifecycleScope.launch{valdeferred=async(Dispatchers.IO){getMessage("LifeCycleKtx")}mMessage.value=deferred.await()}}LifeCycle调用时回到onDestroy(),协程作用域lifecycleScope会自动取消。我们可以在Activity/Fragment等生命周期组件中方便的使用它,但是在MVVM中,我们不会在View层做太多的逻辑处理。viewModelScope基本可以满足ViewModel的需求,lifecycleScope也显得有点太吃货了。无味。但是他有一个特殊的用法:suspendfunLifecycle.whenCreated()suspendfunLifecycle.whenStarted()suspendfunLifecycle.whenResumed()suspendfunLifecycleOwner.whenCreated()suspendfunLifecycleOwner。whenStarted()suspendfunLifecycleOwner.whenResumed()可以指定至少在特定的生命周期后执行suspend函数,可以进一步减轻View层的负担。