协程是一种并发设计模式,你可以在 Android 平台上使用它来简化异步执行的代码。协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念。
在 Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率。本文章介绍如何使用 Kotlin 协程解决以下问题,从而让你能够编写出更清晰、更简洁的应用代码。
特点
协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
在 Android
平台上,协程主要用来解决两个问题:
- 处理耗时任务 (
Long running tasks
),这种任务常常会阻塞住主线程; - 保证主线程安全 (
Main-safety
) ,即确保安全地从主线程调用任何 suspend 函数。
创建协程
创建协程这里介绍常用的两种方式:
- CoroutineScope.launch()
- CoroutineScope.async()
这是常用的协程创建方式,launch
构建器适合执行 "一劳永逸" 的工作,意思就是说它可以启动新协程而不将结果返回给调用方;async
构建器可启动新协程并允许您使用一个名为 await
的挂起函数返回 result
。 launch
和 async
之间的很大差异是它们对异常的处理方式不同。如果使用 async
作为最外层协程的开启方式,它期望最终是通过调用 await
来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async
启动新的最外层协程,而不使用 await
,它会静默地将异常丢弃。
关于作用域,更推荐的是在UI组件中使用 LifecycleOwner.lifecycleScope
,在 ViewModel
中使用 ViewModel.viewModelScope
。
CoroutineContext - 协程上下文
CoroutineContext
即协程的上下文,是 Kotlin 协程的一个基本结构单元。巧妙的运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的 Element
实例集合。这个有索引的集合类似于一个介于 set 和 map之间的数据结构。每个 element
在这个集合有一个唯一的 Key 。当多个 element
的 key 的引用相同,则代表属于集合里同一个 element
。它由如下几项构成:
-
Job
: 控制协程的生命周期; -
CoroutineDispatcher
: 向合适的线程分发任务; -
CoroutineName
: 协程的名称,调试的时候很有用; -
CoroutineExceptionHandler
: 处理未被捕捉的异常。
CoroutineContext
有两个非常重要的元素 — Job
和 Dispatcher
,Job
是当前的 Coroutine
实例而 Dispatcher
决定了当前 Coroutine
执行的线程,还可以添加 CoroutineName
,用于调试,添加 CoroutineExceptionHandler
用于捕获异常,它们都实现了 Element
接口。
CoroutineContext
接口的定义如下:
//Persistent context for the coroutine. It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key.
public interface CoroutineContext {
//操作符 get:可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问。
public operator fun <E : Element> get(key: Key<E>): E?
//和 Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力。
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
//操作符 plus:和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的事情需要小心 —— 要注意它们结合的次序,因为这个 + 运算符是不对称的。
public operator fun plus(context: CoroutineContext): CoroutineContext{...}
//返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。
public fun minusKey(key: Key<*>): CoroutineContext
//Key for the elements of CoroutineContext. E is a type of element with this key.
public interface Key<E : Element>
//An element of the CoroutineContext. An element of the coroutine context is a singleton context by itself.
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
某些情况需要一个上下文不持有任何元素,此时就可以使用
EmptyCoroutineContext 对象。可以预见,添加这个对象到另一个上下文不会对其有任何影响。
在任务层级中,每个协程都会有一个父级对象,要么是 CoroutineScope
或者另外一个 coroutine
。然而,实际上协程的父级 CoroutineContext
和父级协程的 CoroutineContext
是不一样的,因为有如下的公式:
父级上下文 = 默认值 + 继承的
CoroutineContext
+ 参数
其中:
- 一些元素包含默认值:
Dispatchers.Default
是默认的CoroutineDispatcher
,以及coroutine
作为默认的CoroutineName
; - 继承的
CoroutineContext
是CoroutineScope
或者其父协程的CoroutineContext
; - 传入协程
builder
的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值。
请注意: CoroutineContext
可以使用 " + " 运算符进行合并。由于 CoroutineContext
是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext
。比如,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")
。
Job & Deferred - 任务
Job
用于处理协程。对于每一个所创建的协程 (通过 launch
或者 async
),它会返回一个 Job
实例,该实例是协程的唯一标识,并且负责管理协程的生命周期。
CoroutineScope.launch
函数返回的是一个 Job
对象,代表一个异步的任务。Job
具有生命周期并且可以取消。 Job
还可以有层级关系,一个 Job
可以包含多个子 Job
,当父 Job
被取消后,所有的子 Job
也会被自动取消;当子 Job
被取消或者出现异常后父 Job
也会被取消。
除了通过 CoroutineScope.launch
来创建 Job
对象之外,还可以通过 Job()
工厂方法来创建该对象。默认情况下,子 Job
的失败将会导致父 Job
被取消,这种默认的行为可以通过 SupervisorJob
来修改。
具有多个子 Job
的父 Job
会等待所有子 Job
完成(或者取消)后,自己才会执行完成。
Job 的状态
一个任务可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问 Job
的属性: isActive
、isCancelled
和 isCompleted
。
如果协程处于活跃状态,协程运行出错或者调用 job.cancel()
都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true
)。当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true
。
Job 的常用函数
这些函数都是线程安全的,所以可以直接在其他 Coroutine
中调用。
-
fun start(): Boolean
调用该函数来启动这个Coroutine
,如果当前Coroutine
还没有执行调用该函数返回 true,如果当前Coroutine
已经执行或者已经执行完毕,则调用该函数返回 false。 -
fun cancel(cause: CancellationException? = null)
通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以进行调试。 -
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandler
通过这个函数可以给Job
设置一个完成通知,当Job
执行完成的时候会同步执行这个通知函数。 回调的通知对象类型为:typealias CompletionHandler = (cause: Throwable?) -> Unit. CompletionHandler
参数代表了Job
是如何执行完成的。cause
有下面三种情况:
- 如果
Job
是正常执行完成的,则cause
参数为 null- 如果
Job
是正常取消的,则cause
参数为CancellationException
对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。- 其他情况表示
Job
执行失败了。
这个函数的返回值为
DisposableHandle
对象,如果不再需要监控Job
的完成情况了, 则可以调用DisposableHandle.dispose
函数来取消监听。如果Job
已经执行完了, 则无需调用dispose
函数了,会自动取消监听。
-
suspend fun join()
join
函数和前面三个函数不同,这是一个suspend
函数。所以只能在Coroutine
内调用。
这个函数会暂停当前所处的Coroutine
直到该Coroutine
执行完成。所以join
函数一般用来在另外一个Coroutine
中等待job
执行完成后继续执行。当Job
执行完成后,job.join
函数恢复,这个时候job
这个任务已经处于完成状态了,而调用job.join
的Coroutine
还继续处于activie
状态。
请注意,只有在其所有子级都完成后,作业才能完成。
该函数的挂起是可以被取消的,并且始终检查调用的Coroutine
的Job
是否取消。如果在调用此挂起函数或将其挂起时,调用Coroutine
的Job
被取消或完成,则此函数将引发CancellationException
。
Deferred
public interface Deferred<out T> : Job {
//用来等待这个Coroutine执行完毕并返回结果。
public val onAwait: SelectClause1<T>
public suspend fun await(): T
//用来获取Coroutine执行的结果。如果Coroutine还没有执行完成则会抛出 IllegalStateException ,如果任务被取消了也会抛出对应的异常。所以在执行这个函数之前,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。
@ExperimentalCoroutinesApi
public fun getCompleted(): T
//获取已完成状态的Coroutine异常信息,如果任务正常执行完成了,则不存在异常信息,返回null。如果还没有处于已完成状态,则调用该函数同样会抛出 IllegalStateException,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?
}
通过使用
async
创建协程可以得到一个有返回值Deferred
,Deferred
接口继承自Job
接口,额外提供了获取Coroutine
返回结果的方法。由于Deferred
继承自Job
接口,所以Job
相关的内容在Deferred
上也是适用的。Deferred
提供了额外三个函数来处理和Coroutine
执行结果相关的操作。
SupervisorJob
/**
* Creates a _supervisor_ job object in an active state.
* Children of a supervisor job can fail independently of each other.
*
* A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children,
* so a supervisor can implement a custom policy for handling failures of its children:
*
* * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context.
* * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value.
*
* If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its
* parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of
* [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
*
* @param parent an optional parent job.
*/
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
该函数创建了一个处于 active
状态的 supervisor job
。如前所述, Job
是有父子关系的,如果子 Job
失败了父 Job
会自动失败,这种默认的行为可能不是我们期望的。比如在 Activity
中有两个子 Job
分别获取一篇文章的评论内容和作者信息。如果其中一个失败了,我们并不希望父 Job
自动取消,这样会导致另外一个子 Job
也被取消。而 SupervisorJob
就是这么一个特殊的 Job
,里面的子 Job
不相互影响,一个子 Job
失败了,不影响其他子 Job
的执行。SupervisorJob(parent:Job?)
具有一个 parent
参数,如果指定了这个参数,则所返回的 Job
就是参数 parent
的子Job
。如果 Parent Job
失败了或者取消了,则这个 Supervisor Job
也会被取消。当 Supervisor Job
被取消后,所有 Supervisor Job
的子 Job
也会被取消。
MainScope()
的实现就使用了 SupervisorJob
和一个 Main Dispatcher
:
/**
* Creates the main [CoroutineScope] for UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
* ```
*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
但是SupervisorJob
是很容易被误解的,它和协程异常处理、子协程所属Job
类型还有域有很多让人混淆的地方,具体异常处理可以看Google的这一篇文章:协程中的取消和异常 | 异常处理详解
CoroutineDispatcher - 调度器
CoroutineDispatcher
定义了Coroutine
执行的线程。CoroutineDispatcher
可以限定Coroutine
在某一个线程执行、也可以分配到一个线程池来执行、也可以不限制其执行的线程。
CoroutineDispatcher
是一个抽象类,所有dispatcher
都应该继承这个类来实现对应的功能。Dispatchers
是一个标准库中帮我们封装了切换线程的帮助类,可以简单理解为一个线程池。
-
Dispatchers.Default
默认的调度器,适合处理后台计算,是一个CPU
密集型任务调度器。如果创建Coroutine
的时候没有指定dispatcher
,则一般默认使用这个作为默认值。Default dispatcher
使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。 -
Dispatchers.IO
顾名思义这是用来执行阻塞IO
操作的,是和Default
共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。 -
Dispatchers.Unconfined
由于Dispatchers.Unconfined
未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume
的线程决定恢复协程的线程。 -
Dispatchers.Main:
指定执行的线程是主线程,在Android
上就是UI
线程·
由于 子Coroutine
会继承 父Coroutine
的 context
,所以为了方便使用,我们一般会在 父Coroutine
上设定一个 Dispatcher
,然后所有 子Coroutine
自动使用这个 Dispatcher
。
CoroutineStart - 协程启动模式
-
CoroutineStart.DEFAULT:
协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态。
虽然是立即调度,但也有可能在执行前被取消。 -
CoroutineStart.ATOMIC:
协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消。
虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行。 -
CoroutineStart.LAZY:
只要协程被需要时,包括主动调用该协程的start
、join
或者await
等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 -
CoroutineStart.UNDISPATCHED:
协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点。
是立即执行,因此协程一定会执行。
这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用
DEFAULT
和LAZY
这两个启动模式就够了。
CoroutineScope - 协程作用域
定义协程必须指定其 CoroutineScope
。CoroutineScope
可以对协程进行追踪,即使协程被挂起也是如此。同调度程序 (Dispatcher
) 不同,CoroutineScope
并不运行协程,它只是确保您不会失去对协程的追踪。为了确保所有的协程都会被追踪,Kotlin
不允许在没有使用 CoroutineScope
的情况下启动新的协程。CoroutineScope
可被看作是一个具有超能力的 ExecutorService
的轻量级版本。CoroutineScope
会跟踪所有协程,同样它还可以取消由它所启动的所有协程。这在 Android
开发中非常有用,比如它能够在用户离开界面时停止执行协程。
Coroutine
是轻量级的线程,并不意味着就不消耗系统资源。 当异步操作比较耗时的时候,或者当异步操作出现错误的时候,需要把这个 Coroutine
取消掉来释放系统资源。在 Android
环境中,通常每个界面(Activity
、Fragment
等)启动的 Coroutine
只在该界面有意义,如果用户在等待 Coroutine
执行的时候退出了这个界面,则再继续执行这个 Coroutine
可能是没必要的。另外 Coroutine
也需要在适当的 context
中执行,否则会出现错误,比如在非 UI
线程去访问 View
。 所以 Coroutine
在设计的时候,要求在一个范围(Scope
)内执行,这样当这个 Scope
取消的时候,里面所有的子 Coroutine
也自动取消。所以要使用 Coroutine
必须要先创建一个对应的 CoroutineScope
。
CoroutineScope 接口
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope
只是定义了一个新 Coroutine
的执行 Scope
。每个 coroutine builder
都是 CoroutineScope
的扩展函数,并且自动的继承了当前 Scope
的 coroutineContext
。
分类及行为规则
官方框架在实现复合协程的过程中也提供了作用域,主要用以明确写成之间的父子关系,以及对于取消或者异常处理等方面的传播行为。该作用域包括以下三种:
-
顶级作用域
没有父协程的协程所在的作用域为顶级作用域。 -
协同作用域
协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。 -
主从作用域
与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。
除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
- 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
- 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
- 子协程会继承父协程的协程上下文中的元素,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。
常用作用域
官方库给我们提供了一些作用域可以直接来使用,并且 Android 的Lifecycle Ktx库也封装了更好用的作用域,下面看一下各种作用域
GlobalScope - 不推荐使用
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
GlobalScope
是一个单例实现,源码十分简单,上下文是 EmptyCoroutineContext
,是一个空的上下文,切不包含任何 Job
,该作用域常被拿来做示例代码,由于 GlobalScope
对象没有和应用生命周期组件相关联,需要自己管理 GlobalScope
所创建的 Coroutine
,且 GlobalScope
的生命周期是 process
级别的,所以一般而言我们不推荐使用 GlobalScope
来创建 Coroutine
。
runBlocking{} - 主要用于测试
/**
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
* This function should not be used from a coroutine. It is designed to bridge regular blocking code
* to libraries that are written in suspending style, to be used in `main` functions and in tests.
*
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
* in this blocked thread until the completion of this coroutine.
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
*
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
* then this invocation uses the outer event loop.
*
* If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
* this `runBlocking` invocation throws [InterruptedException].
*
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
* for a newly created coroutine.
*
* @param context the context of the coroutine. The default value is an event loop on the current thread.
* @param block the coroutine code.
*/
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val currentThread = Thread.currentThread()
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
if (contextInterceptor == null) {
// create or use private event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
// See if context's interceptor is an event loop that we shall use (to support TestContext)
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
这是一个顶层函数,从源码的注释中我们可以得到一些信息,运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(suspending style
)编写的库,以用于主函数与测试。该函数主要用于测试,不适用于日常开发,该协程会阻塞当前线程直到协程体执行完成。
MainScope() - 可用于开发
/**
* Creates the main [CoroutineScope] for UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
* ```
*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
该函数是一个顶层函数,用于返回一个上下文是 SupervisorJob() + Dispatchers.Main
的作用域,该作用域常被使用在 Activity/Fragment
,并且在界面销毁时要调用 fun CoroutineScope.cancel(cause: CancellationException? = null)
对协程进行取消,这是官方库中可以在开发中使用的一个用于获取作用域的顶层函数,使用示例在官方库的代码注释中已经给出,上面的源码中也有,使用起来也是十分的方便。
LifecycleOwner.lifecycleScope - 推荐使用
/**
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
*/
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
该扩展属性是 Android
的 Lifecycle Ktx
库提供的具有生命周期感知的协程作用域,它与 LifecycleOwner
的 Lifecycle
绑定,Lifecycle
被销毁时,此作用域将被取消。这是在 Activity/Fragment
中推荐使用的作用域,因为它会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,相同作用的还有下文提到的 ViewModel.viewModelScope
。
ViewModel.viewModelScope - 推荐使用
/**
* [CoroutineScope] tied to this [ViewModel].
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
该扩展属性和上文中提到的 LifecycleOwner.lifecycleScope
基本一致,它是 ViewModel
的扩展属性,也是来自 Android
的 Lifecycle Ktx
库,它能够在此 ViewModel
销毁时自动取消,同样不会造成协程泄漏。该扩展属性返回的作用域的上下文同样是 SupervisorJob() + Dispatchers.Main.immediate
。
coroutineScope & supervisorScope
/**
* Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* context's [Job] with [SupervisorJob].
*
* A failure of a child does not cause this scope to fail and does not affect its other children,
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
* A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
* but does not cancel parent job.
*/
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
/**
* Creates a [CoroutineScope] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* the context's [Job].
*
* This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
* This function returns as soon as the given block and all its children coroutines are completed.
* A usage example of a scope looks like this:
*
* ```
* suspend fun showSomeData() = coroutineScope {
* val data = async(Dispatchers.IO) { // <- extension on current scope
* ... load some UI data for the Main thread ...
* }
*
* withContext(Dispatchers.Main) {
* doSomeWork()
* val result = data.await()
* display(result)
* }
* }
* ```
*
* The scope in this example has the following semantics:
* 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
* 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
* 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
* 4) If the `async` block fails, `withContext` will be cancelled.
*
* The method may throw a [CancellationException] if the current job was cancelled externally
* or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
*/
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
首先这两个函数都是挂起函数,需要运行在协程内或挂起函数内。supervisorScope
属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,它的设计应用场景多用于子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。coroutineScope
和 supervisorScope
都会返回一个作用域,它俩的差别就是异常传播:coroutineScope
内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出;supervisorScope
内部的异常不会向上传播,一个子协程异常退出,不会影响父协程和兄弟协程的运行。
协程的取消和异常
普通协程如果产生未处理异常会将此异常传播至它的父协程,然后父协程会取消所有的子协程、取消自己、将异常继续向上传递
这种情况有的时候并不是我们想要的,我们更希望一个协程在产生异常时,不影响其他协程的执行,在上文中我们也提到了一些解决方案,下面我们就在实践一下。
使用SupervisorJob**
在上文中我们也对这个顶层函数做了讲解,那如何使用呢?直接上代码:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
/**
* 使用官方库的 MainScope()获取一个协程作用域用于创建协程
*/
private val mScope = MainScope()
companion object {
const val TAG = "Kotlin Coroutine"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mScope.launch(Dispatchers.Default) {
delay(500)
Log.e(TAG, "Child 1")
}
mScope.launch(Dispatchers.Default) {
delay(1000)
Log.e(TAG, "Child 2")
throw RuntimeException("--> RuntimeException <--")
}
mScope.launch(Dispatchers.Default) {
delay(1500)
Log.e(TAG, "Child 3")
}
}
}
打印结果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
Process: com.quyunshuo.kotlincoroutine, PID: 24240
java.lang.RuntimeException: --> RuntimeException <--
at com.quyunshuo.kotlincoroutine.MainActivity$onCreate$2.invokeSuspend(MainActivity.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
E/Kotlin Coroutine: Child 3
MainScope()
我们之前提到过了,它的实现就是用了 SupervisorJob
。执行结果就是 Child 2 抛出异常后,Child 3 正常执行了,但是程序崩了,因为我们没有处理这个异常,下面完善一下代码
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mScope.launch(Dispatchers.Default) {
delay(500)
Log.e(TAG, "Child 1")
}
// 在Child 2的上下文添加了异常处理
mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "CoroutineExceptionHandler: $throwable")
}) {
delay(1000)
Log.e(TAG, "Child 2")
throw RuntimeException("--> RuntimeException <--")
}
mScope.launch(Dispatchers.Default) {
delay(1500)
Log.e(TAG, "Child 3")
}
}
输出结果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3
这一次,程序没有崩溃,并且异常处理的打印也输出了,这就达到了我们想要的效果。但是要注意一个事情,这几个子协程的父级是 SupervisorJob
,但是他们再有子协程的话,他们的子协程的父级就不是 SupervisorJob
了,所以当它们产生异常时,就不是我们演示的效果了。
新的协程被创建时,会生成新的 Job
实例替代 SupervisorJob
。
使用supervisorScope
这个作用域我们上文中也有提到,使用 supervisorScope
也可以达到我们想要的效果,上代码:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "Kotlin Coroutine"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "CoroutineExceptionHandler: $throwable")
}) {
supervisorScope {
launch {
delay(500)
Log.e(TAG, "Child 1 ")
}
launch {
delay(1000)
Log.e(TAG, "Child 2 ")
throw RuntimeException("--> RuntimeException <--")
}
launch {
delay(1500)
Log.e(TAG, "Child 3 ")
}
}
}
}
}
输出结果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3
可以看到已经达到了我们想要的效果,但是如果将 supervisorScope
换成 coroutineScope
,结果就不是这样了。
在后台线程中执行
如果在主线程上发出网络请求,则主线程会处于等待或阻塞状态,直到收到响应。由于线程处于阻塞状态,因此操作系统无法调用 onDraw()
,这会导致应用冻结,并有可能导致弹出“应用无响应”(ANR
) 对话框。为了提供更好的用户体验,我们在后台线程上执行此操作。
首先,我们来了解一下 Repository
类,看看它是如何发出网络请求的:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
makeLoginRequest
是同步的,并且会阻塞发起调用的线程。为了对网络请求的响应建模,我们创建了自己的 Result
类。
ViewModel
会在用户点击(例如,点击按钮)时触发网络请求:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
使用上述代码,LoginViewModel
会在网络请求发出时阻塞界面线程。如需将执行操作移出主线程,最简单的方法是创建一个新的协程,然后在 I/O 线程上执行网络请求:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
下面我们仔细分析一下 login
函数中的协程代码:
viewModelScope
是预定义的 CoroutineScope
,包含在 ViewModel
KTX 扩展中。请注意,所有协程都必须在一个作用域内运行。一个 CoroutineScope
管理一个或多个相关的协程。
launch
是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序。
Dispatchers.IO
指示此协程应在为 I/O 操作预留的线程上执行。
login
函数按以下方式执行:
应用从主线程上的 View
层调用 login
函数。
launch
会创建一个新的协程,并且网络请求在为 I/O 操作预留的线程上独立发出。
在该协程运行时,login
函数会继续执行,并可能在网络请求完成前返回。请注意,为简单起见,我们暂时忽略掉网络响应。
由于此协程通过 viewModelScope
启动,因此在 ViewModel
的作用域内执行。如果 ViewModel
因用户离开屏幕而被销毁,则 viewModelScope
会自动取消,且所有运行的协程也会被取消。
前面的示例存在的一个问题是,调用 makeLoginRequest
的任何项都需要记得将执行操作显式移出主线程。下面我们来看看如何修改 Repository
以解决这一问题。
使用协程确保主线程安全
如果函数不会在主线程上阻止界面更新,我们即将其视为是主线程安全的。makeLoginRequest
函数不是主线程安全的,因为从主线程调用 makeLoginRequest
确实会阻塞界面。可以使用协程库中的 withContext()
函数将协程的执行操作移至其他线程:
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// 将协程的执行移至 I/O 调度器
return withContext(Dispatchers.IO) {
// 阻止网络请求代码
}
}
}
withContext(Dispatchers.IO)
将协程的执行操作移至一个 I/O 线程,这样一来,我们的调用函数便是主线程安全的,并且支持根据需要更新界面。
makeLoginRequest
还会用 suspend
关键字进行标记。Kotlin 利用此关键字强制从协程内调用函数。
注意:为更轻松地进行测试,我们建议将 Dispatchers
注入 Repository
层。如需了解详情,请参阅在 Android 上测试协程。
在以下示例中,协程是在 LoginViewModel
中创建的。由于 makeLoginRequest
将执行操作移出主线程,login
函数中的协程现在可以在主线程中执行:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
请注意,此处仍需要协程,因为 makeLoginRequest
是一个 suspend
函数,而所有 suspend
函数都必须在协程中执行。
此代码与前面的 login
示例的不同之处体现在以下几个方面:
-
launch
不接受Dispatchers.IO
参数。如果您未将Dispatcher
传递至launch
,则从viewModelScope
启动的所有协程都会在主线程中运行。 - 系统现在会处理网络请求的结果,以显示成功或失败界面。
login 函数现在按以下方式执行:
- 应用从主线程上的
View
层调用login()
函数。 -
launch
创建一个新的协程,以在主线程上发出网络请求,然后该协程开始执行。 - 在协程内,调用
loginRepository.makeLoginRequest()
现在会挂起协程的进一步执行操作,直至makeLoginRequest()
中的withContext
块结束运行。 -
withContext
块结束运行后,login()
中的协程在主线程上恢复执行操作,并返回网络请求的结果。
注意:如需与 ViewModel
层中的 View
通信,请按照应用架构指南中的建议,使用 LiveData
。遵循此模式时,ViewModel
中的代码会在主线程上执行,因此您可以直接调用 MutableLiveData
的 setValue()
函数。
处理异常
为了处理 Repository
层可能抛出的异常,请使用 Kotlin 对异常的内置支持。在以下示例中,我们使用的是 try-catch
块:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun makeLoginRequest(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
在此示例中,makeLoginRequest()
调用抛出的任何意外异常都会处理为界面错误。
将 Kotlin 协程与生命周期感知型组件一起使用
Kotlin 协程提供了一个可供您编写异步代码的 API。通过 Kotlin 协程,您可以定义 CoroutineScope
,以帮助您管理何时应运行协程。每个异步操作都在特定范围内运行。
生命周期感知型组件针对应用中的逻辑范围以及与 LiveData
的互操作层为协程提供了一流的支持。本文章会介绍如何有效地结合使用协程与生命周期感知型组件。
生命周期感知型协程范围
命周期感知型组件定义了以下内置范围供您在应用中使用。
ViewModelScope
为应用中的每个 ViewModel
定义了 ViewModelScope
。如果 ViewModel
已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel
处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel
,以便在 ViewModel
清除后,系统会自动取消工作以避免消耗资源。
您可以通过 ViewModel
的 viewModelScope
属性访问 ViewModel
的 CoroutineScope
,如以下示例所示:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
为每个 Lifecycle
对象定义了 LifecycleScope
。在此范围内启动的协程会在 Lifecycle
被销毁时取消。您可以通过 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
属性访问 Lifecycle
的 CoroutineScope
。
以下示例演示了如何使用 lifecycleOwner.lifecycleScope
异步创建预计算文本:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
可重启生命周期感知型协程
即使 lifecycleScope
提供了适当的方法以在 Lifecycle
处于 DESTROYED
状态时自动取消长时间运行的操作,但在某些情况下,您可能需要在 Lifecycle
处于某个特定状态时开始执行代码块,并在其处于其他状态时取消。例如,您可能希望在 Lifecycle
处于 STARTED
状态时收集数据流,并在其处于 STOPPED
状态时取消收集。此方法仅在界面显示在屏幕上时才处理数据流发出操作,这样可节省资源并可能会避免发生应用崩溃问题。
对于这些情况,Lifecycle
和 LifecycleOwner
提供了挂起 repeatOnLifecycle
API 来确切实现相应操作。以下示例中的代码块会在每次关联的 Lifecycle
至少处于 STARTED
状态时运行,并且会在 Lifecycle
处于 STOPPED
状态时取消运行:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 在生命周期范围内创建一个新的协程
viewLifecycleOwner.lifecycleScope.launch {
// 每次生命周期处于 STARTED 状态(或更高)时,
// repeatOnLifecycle 在新的协程中启动块,并在它停止时取消它。
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 触发流程并开始监听值。
// 当生命周期开始时会发生这种情况,当生命周期停止时会停止收集
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
生命周期感知型数据流收集
如果你只需要对单个数据流执行生命周期感知型收集,可以使用 Flow.flowWithLifecycle()
方法简化代码:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// 处理值
}
}
但是,如果你需要并行对多个数据流执行生命周期感知型收集,则必须在不同的协程中收集每个数据流。在这种情况下,直接使用 repeatOnLifecycle()
会更加高效:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 因为 collect 是一个挂起函数,所以如果要并行收集多个流,则需要在不同的协程中进行。
launch {
flow1.collect {
// 处理值
}
}
launch {
flow2.collect {
// 处理值
}
}
}
}
挂起生命周期感知型协程
即使 CoroutineScope
提供了适当的方法来自动取消长时间运行的操作,在某些情况下,你可能需要暂停执行代码块(除非 Lifecycle
处于特定状态)。例如,如需运行 FragmentTransaction
,您必须等到 Lifecycle
至少为 STARTED
。对于这些情况,Lifecycle
提供了其他方法:lifecycle.whenCreated
、lifecycle.whenStarted
和 lifecycle.whenResumed
。如果 Lifecycle
未至少处于所需的最低状态,则会挂起在这些块内运行的任何协程。
以下示例包含仅当关联的 Lifecycle
至少处于 STARTED
状态时才会运行的代码块:
class MyFragment: Fragment {
init { // 请注意,我们可以在 Fragment 的构造函数中安全地启动。
lifecycleScope.launch {
whenStarted {
// 只有在 Lifecycle 至少 STARTED 时,内部的块才会运行。
// 它将在片段启动时开始执行,并且可以调用其他挂起方法。
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// 当 checkUserAccess 返回时,如果生命周期没有至少 STARTED,则下一行将自动挂起。
// 我们可以安全地运行片段事务,因为我们知道除非生命周期至少已开始,否则代码不会运行。
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// 此行仅在上面的 whenStarted 块完成后运行。
}
}
}
如果在协程处于活动状态时通过某种 when
方法销毁了 Lifecycle
,协程会自动取消。在以下示例中,一旦 Lifecycle
状态变为 DESTROYED
,finally
块即会运行:
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// 调用一些挂起函数。
} finally {
// 此行可能会在 Lifecycle 被 DESTROYED 后执行。
if (lifecycle.state >= STARTED) {
// 在这里,由于我们已经检查过,运行任何 Fragment 事务都是安全的。
}
}
}
}
}
注意:尽管这些方法为使用 Lifecycle
提供了便利,但只有当信息在 Lifecycle
的范围(例如预计算文本)内有效时才应使用它们。请注意,协程不会随着 activity
重启而重启。
警告:倾向于使用 repeatOnLifecycle
API 收集数据流,而不是在 launchWhenX
API 内部进行收集。由于后面的 API 会挂起协程,而不是在 Lifecycle
处于 STOPPED
状态时取消。上游数据流会在后台保持活跃状态,并可能会发出新的项并耗用资源。
将协程与 LiveData 一起使用
使用 LiveData
时,您可能需要异步计算值。例如,您可能需要检索用户的偏好设置并将其传送给界面。在这些情况下,您可以使用 liveData
构建器函数调用 suspend
函数,并将结果作为 LiveData
对象传送。
在以下示例中,loadUser()
是在其他位置声明的挂起函数。使用 liveData
构建器函数异步调用 loadUser()
,然后使用 emit()
发出结果:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
liveData
构建块用作协程和 LiveData
之间的结构化并发基元。当 LiveData
变为活动状态时,代码块开始执行;当 LiveData
变为非活动状态时,代码块会在可配置的超时过后自动取消。如果代码块在完成前取消,则会在 LiveData
再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。请注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancellationException
)而取消,则不会重启。
你还可以从代码块中发出多个值。每次 emit()
调用都会挂起代码块的执行,直到在主线程上设置 LiveData
值。
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
您也可以将 liveData
与 Transformations
结合使用,如以下示例所示:
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
中发出多个值,方法是在每次想要发出新值时调用 emitSource()
函数。请注意,每次调用 emit()
或 emitSource()
都会移除之前添加的来源。
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// 停止先前的发射以避免将更新的用户作为“加载”调度。
disposable.dispose()
// 更新数据库。
userDao.insert(user)
// 使用成功类型重新建立发射。
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// 任何对 `emit` 的调用都会自动释放前一个,因此我们不需要在此处释放它,
// 因为我们没有获得更新的值。
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
相关官方文档、文章链接:
https://kotlinlang.org/docs/coroutines-guide.html
https://developer.android.com/kotlin/coroutines
https://juejin.cn/post/6950616789390721037
网友评论