Kotlin Coroutines在Android中的实践
coroutines在android中的实践
前面两篇文章讲了协程的基础知识和协程的通信.
见:
- kotlin coroutines不复杂, 我来帮你理一理
-
kotlin协程通信机制: channel
举的例子可能离实际的应用代码比较遥远.
这篇我们就从android应用的角度, 看看实践中都有哪些地方可以用到协程.
coroutines的用途
coroutines在android中可以帮我们做什么:
- 取代callbacks, 简化代码, 改善可读性.
- 保证main safety.
- 结构化管理和取消任务, 避免泄漏.
这有一个例子:
suspend fun fetchdocs() { // dispatchers.main val result = get("developer.android.com") // dispatchers.main show(result) // dispatchers.main } suspend fun get(url: string) = // dispatchers.main withcontext(dispatchers.io) { // dispatchers.io (main-safety block) /* perform network io here */ // dispatchers.io (main-safety block) } // dispatchers.main }
这里get
是一个suspend
方法, 只能在另一个suspend
方法或者在一个协程中调用.
get
方法在主线程被调用, 它在开始请求之前suspend了协程, 当请求返回, 这个方法会resume协程, 回到主线程. 网络请求不会block主线程.
main-safety是如何保证的呢?
dispatcher决定了协程在什么线程上执行. 每个协程都有dispatcher. 协程suspend自己, dispatcher负责resume它们.
-
dispatchers.main
: 主线程: ui交互, 更新livedata
, 调用suspend
方法等. -
dispatchers.io
: io操作, 数据库操作, 读写文件, 网路请求. -
dispatchers.default
: 主线程之外的计算任务(cpu-intensive work), 排序, 解析json等.
一个好的实践是使用withcontext()
来确保每个方法都是main-safe的, 调用者可以在主线程随意调用, 不用关心里面的代码到底是哪个线程的.
管理协程
之前讲scope和structured concurrency的时候提过, scope最典型的应用就是按照对象的生命周期, 自动管理其中的协程, 及时取消, 避免泄漏和冗余操作.
在协程之中再启动新的协程, 父子协程是共享scope的, 也即scope会track其中所有的协程.
协程被取消会抛出cancellationexception
.
coroutinescope
和supervisorscope
可以用来在suspend方法中启动协程. structured concurrency保证: 当一个suspend函数返回时, 它的所有工作都执行完毕.
它们两者的区别是: 当子协程发生错误的时候, coroutinescope
会取消scope中的所有的子协程, 而supervisorscope
不会取消没有发生错误的其他子协程.
activity/fragment & coroutines
在android中, 可以把一个屏幕(activity/fragment)和一个coroutinescope
关联, 这样在activity或fragment生命周期结束的时候, 可以取消这个scope下的所有协程, 好避免协程泄漏.
利用coroutinescope
来做这件事有两种方法: 创建一个coroutinescope
对象和activity的生命周期绑定, 或者让activity实现coroutinescope
接口.
方法1: 持有scope引用:
class activity { private val mainscope = mainscope() fun destroy() { mainscope.cancel() } }
方法2: 实现接口:
class activity : coroutinescope by coroutinescope(dispatchers.default) { fun destroy() { cancel() // extension on coroutinescope } }
默认线程可以根据实际的需要指定.
fragment的实现类似, 这里不再举例.
viewmodel & coroutines
google目前推广的mvvm模式, 由viewmodel来处理逻辑, 在viewmodel中使用协程, 同样也是利用scope来做管理.
viewmodel在屏幕旋转的时候并不会重建, 所以不用担心协程在这个过程中被取消和重新开始.
方法1: 自己创建scope
private val viewmodeljob = job() private val uiscope = coroutinescope(dispatchers.main + viewmodeljob)
默认是在ui线程.coroutinescope
的参数是coroutinecontext
, 是一个配置属性的集合. 这里指定了dispatcher和job.
在viewmodel被销毁的时候:
override fun oncleared() { super.oncleared() viewmodeljob.cancel() }
这里viewmodeljob是uiscope的job, 取消了viewmodeljob, 所有这个scope下的协程都会被取消.
一般coroutinescope
创建的时候会有一个默认的job, 可以这样取消:
uiscope.coroutinecontext.cancel()
方法2: 利用viewmodelscope
如果我们用上面的方法, 我们需要给每个viewmodel都这样写. 为了避免这些boilerplate code, 我们可以用viewmodelscope
.
注: 要使用viewmodelscope需要添加相应的ktx依赖.
- for viewmodelscope, use
androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01
or higher.
viewmodelscope
绑定的是dispatchers.main
, 会自动在viewmodel clear的时候自动取消.
用的时候直接用就可以了:
class mainviewmodel : viewmodel() { // make a network request without blocking the ui thread private fun makenetworkrequest() { // launch a coroutine in viewmodelscope viewmodelscope.launch(dispatchers.io) { // slowfetch() } } // no need to override oncleared() }
所有的setting up和clearing工作都是库完成的.
lifecyclescope & coroutines
每一个lifecycle对象都有一个lifecyclescope
.
同样也需要添加依赖:
- for lifecyclescope, use
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01
or higher.
要访问coroutinescope
可以用lifecycle.coroutinescope
或者lifecycleowner.lifecyclescope
属性.
比如:
activity.lifecyclescope.launch {} fragment.lifecyclescope.launch {} fragment.viewlifecycleowner.launch {}
lifecyclescope
可以启动协程, 当lifecycle结束的时候, 任何这个scope中启动的协程都会被取消.
这比较适合于处理一些带delay的ui操作, 比如需要用handler.postdelayed的更新ui的操作, 有多个操作的时候嵌套难看, 还容易有泄漏问题.
用了lifecyclescope之后, 既避免了嵌套代码, 又自动处理了取消.
lifecyclescope.launch { delay(delay) showfullhint() delay(delay) showsmallhint() }
lifecyclescope和viewmodelscope
但是lifecyclescope启动的协程却不适合调用repository的方法. 因为它的生命周期和activity/fragment是一致的, 太碎片化了, 容易被取消, 造成浪费.
设备旋转时, activity会被重建, 如果取消请求再重新开始, 会造成一种浪费.
可以把请求放在viewmodel中, ui层重新注册获取结果. viewmodelscope
和lifecyclescope
可以结合起来使用.
举例: viewmodel这样写:
class noteviewmodel: viewmodel { val notedeferred = completabledeferred<note>() viewmodelscope.launch { val note = repository.loadnote() notedeferred.complete(note) } suspend fun loadnote(): note = notedeferred.await() }
而我们的ui中:
fun oncreate() { lifecyclescope.launch { val note = userviewmodel.loadnote() updateui(note) } }
这样做之后的好处:
- viewmodel保证了数据请求没有浪费, 屏幕旋转不会重新发起请求.
- lifecyclescope保证了view没有leak.
特定生命周期阶段
尽管scope提供了自动取消的方式, 你可能还有一些需求需要限制在更加具体的生命周期内.
比如, 为了做fragmenttransaction
, 你必须等到lifecycle
至少是started
.
上面的例子中, 如果需要打开一个新的fragment:
fun oncreate() { lifecyclescope.launch { val note = userviewmodel.loadnote() fragmentmanager.begintransaction()....commit() //illegalstateexception } }
很容易发生illegalstateexception
.
lifecycle提供了:lifecycle.whencreated
, lifecycle.whenstarted
, lifecycle.whenresumed
.
如果没有至少达到所要求的最小生命周期, 在这些块中启动的协程任务, 将会suspend.
所以上面的例子改成这样:
fun oncreate() { lifecyclescope.launchwhenstarted { val note = userviewmodel.loadnote() fragmentmanager.begintransaction()....commit() } }
如果lifecycle
对象被销毁(state==destroyed
), 这些when方法中的协程也会被自动取消.
livedata & coroutines
livedata
是一个供ui观察的value holder.
livedata
的数据可能是异步获得的, 和协程结合:
val user: livedata<user> = livedata { val data = database.loaduser() // loaduser is a suspend function. emit(data) }
这个例子中的livedata
是一个builder function, 它调用了读取数据的方法(一个suspend
方法), 然后用emit()
来发射结果.
同样也是需要添加依赖的:
- for livedata, use
androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01
or higher.
实际上使用时, 可以emit()
多次:
val user: livedata<result> = livedata { emit(result.loading()) try { emit(result.success(fetchuser())) } catch(ioexception: exception) { emit(result.error(ioexception)) } }
每次emit()
调用都会suspend这个块, 直到livedata
的值在主线程被设置.
livedata
还可以做变换:
class myviewmodel: viewmodel() { private val userid: livedata<string> = mutablelivedata() val user = userid.switchmap { id -> livedata(context = viewmodelscope.coroutinecontext + dispatchers.io) { emit(database.loaduserbyid(id)) } } }
如果数据库的方法返回的类型是livedata类型, emit()
方法可以改成emitsource()
. 例子见: use coroutines with livedata.
网络/数据库 & coroutines
根据architecture components的构建模式:
- viewmodel负责在主线程启动协程, 清理时取消协程, 收到数据时用
livedata
传给ui. - repository暴露
suspend
方法, 确保方法main-safe. - 数据库和网络暴露
suspend
方法, 确保方法main-safe. room和retrofit都是符合这个pattern的.
repository暴露suspend
方法, 是主线程safe的, 如果要对结果做一些heavy的处理, 比如转换计算, 需要用withcontext
自行确定主线程不被阻塞.
retrofit & coroutines
retrofit从2.6.0开始提供了对协程的支持.
定义方法的时候加上suspend
关键字:
interface githubservice { @get("orgs/{org}/repos?per_page=100") suspend fun getorgrepos( @path("org") org: string ): list<repo> }
suspend方法进行请求的时候, 不会阻塞线程.
返回值可以直接是结果类型, 或者包一层response
:
@get("orgs/{org}/repos?per_page=100") suspend fun getorgrepos( @path("org") org: string ): response<list<repo>>
room & coroutines
room从2.1.0版本开始提供对协程的支持. 具体就是dao方法可以是suspend
的.
@dao interface usersdao { @query("select * from users") suspend fun getusers(): list<user> @insert suspend fun insertuser(user: user) @update suspend fun updateuser(user: user) @delete suspend fun deleteuser(user: user) }
room使用自己的dispatcher来确定查询运行在后台线程.
所以你的代码不应该使用withcontext(dispatchers.io)
, 会让代码变得复杂并且查询变慢.
更多内容可见: room