之前有写过一篇文章简单讲解了一下 Flow。其中也提到了 LiveData 和 Flow 的区别。LiveData 使用起来足够简单,但缺点也很明显,那就是无法像 Flow 一样指定工作线程。尽管 LiveData 拥有 postValue() 方法允许开发者在子线程更新 LiveData。但是涉及到 LiveData 的 Transformation 时,所有的操作都是在主线程进行的。这里借用 5 Uses of KTX LiveData Coroutine Builder 一文中的图片:

这时有同学会说那我用 Flow 好了。然而,Flow 虽然可以使用 flowOn() 来指定工作线程,加上 repeatOnLifeCycle() 后也可以和 LiveData 一样拥有感知页面生命周期的能力。但由于 Flow 是建立在 Kotlin Coroutine 的基础之上。这就导致了在 Java 中无法使用 Flow。当然如果是纯 Kotlin 项目,那当我没说,这种场景下我们完完全全可以使用 Flow。
Google 的 JetPack 团队也考虑到了这个问题,于是在 lifecycle-livedata-ktx 库中提供了将 LiveData 与 Flow 相连接的方法。也就是今天的主角:CoroutineLiveData
使用入门
首先添加依赖 androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 到项目中。
再然后我们就可以非常愉快的使用了:
val flowOne = flow {
emit("Hello")
emit("World")
}.flowOn(Dispatchers.IO)
val liveDataOne = flowOne.asLiveData()
// or
val liveDataTwo = liveData(Dispatchers.IO) {
emit("Again")
emitSource(liveDataOne)
}
我们一个一个来说。首先是 asLiveData() 方法。它可以将一个 Flow 转换成一个 LiveData 供我们使用。
// 注释很有用,篇幅原因这里就不贴出来了。
@JvmOverloads
public fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
我们发现 asLiveData 方法其实是第二种写法的一层封装,所以我们直接看第二个方法:
public fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
这个方法接受三个参数,timeoutInMs 我们讲 Flow 时见过类似的参数,用于延迟一段时间后再取消 flow。DEFAULT_TIMEOUT 为 5000ms。context 也没什么好说的,需要注意一点就是可能需要设置一个 CoroutineExceptionHandler 来处理异常,否则的话 Flow 的异常会抛到主线程的 UncaughtExceptionHandler 去。也可以通过在原 Flow 上添加一个 catch 运算符来解决。
someFlow.asLiveData(CoroutineExceptionHandler { context, throwable ->
TODO()
})
// 或
someFlow.catch{ TODO() }
至于 block 则比较关键,LiveDataScope:它是一个接口,定义了两个方法:
public interface LiveDataScope<T> {
public suspend fun emit(value: T)
public suspend fun emitSource(source: LiveData<T>): DisposableHandle
public val latestValue: T?
}
首先,注意下这两个方法是有 suspend 关键字的,也就是说在这个 LiveData 里,我们可以使用协程。
另外可以看到,在这里我们不仅可以 emit 一个值,我们还可以 emitSource 一个 LiveData。这个方法和 MediatorLiveData 很像,但是存在一些差别。那就是 emitSource 后,如果再次调用 emit 或 emitSource 后,都会移除前面的 source。而 MediatorLiveData 的 addSource 是可以存在多个 source 的。
最后我们重点看下 CoroutineLiveData
CoroutineLiveData 继承自 MediatorLiveData:
internal class CoroutineLiveData<T>(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
block: Block<T>
) : MediatorLiveData<T>() {
private var blockRunner: BlockRunner<T>?
private var emittedSource: EmittedSource? = null
init {
// use an intermediate supervisor job so that if we cancel individual block runs due to losing
// observers, it won't cancel the given context as we only cancel w/ the intention of possibly
// relaunching using the same parent context.
val supervisorJob = SupervisorJob(context[Job])
// The scope for this LiveData where we launch every block Job.
// We default to Main dispatcher but developer can override it.
// The supervisor job is added last to isolate block runs.
val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
blockRunner = BlockRunner(
liveData = this,
block = block,
timeoutInMs = timeoutInMs,
scope = scope
) {
blockRunner = null
}
}
internal suspend fun emitSource(source: LiveData<T>): DisposableHandle {
clearSource()
val newSource = addDisposableSource(source)
emittedSource = newSource
return newSource
}
internal suspend fun clearSource() {
emittedSource?.disposeNow()
emittedSource = null
}
override fun onActive() {
super.onActive()
blockRunner?.maybeRun()
}
override fun onInactive() {
super.onInactive()
blockRunner?.cancel()
}
}
可以看到主要逻辑其实在 blockRunner 和 emittedSource 里。onActive() 与 onInactive() 方法可以获取到 LiveData 的运行状态。CoroutineLiveData 在这里调用 blockRunner 的 maybeRun() 和 cancel() 来控制启动和取消。
那么我们就需要看一看 BlockRunner 了:
internal class BlockRunner<T>(
private val liveData: CoroutineLiveData<T>,
private val block: Block<T>,
private val timeoutInMs: Long,
private val scope: CoroutineScope,
private val onDone: () -> Unit
) {
// currently running block job.
private var runningJob: Job? = null
// cancelation job created in cancel.
private var cancellationJob: Job? = null
@MainThread
fun maybeRun() {
cancellationJob?.cancel()
cancellationJob = null
if (runningJob != null) {
return
}
runningJob = scope.launch {
val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext)
block(liveDataScope)
onDone()
}
}
@MainThread
fun cancel() {
if (cancellationJob != null) {
error("Cancel call cannot happen without a maybeRun")
}
cancellationJob = scope.launch(Dispatchers.Main.immediate) {
delay(timeoutInMs)
if (!liveData.hasActiveObservers()) {
// one last check on active observers to avoid any race condition between starting
// a running coroutine and cancelation
runningJob?.cancel()
runningJob = null
}
}
}
}
逻辑不复杂,所以想要研究明白我们还得继续往下走,看一看 LiveDataScopeImpl:
internal class LiveDataScopeImpl<T>(
internal var target: CoroutineLiveData<T>,
context: CoroutineContext
) : LiveDataScope<T> {
override val latestValue: T?
get() = target.value
// use `liveData` provided context + main dispatcher to communicate with the target
// LiveData. This gives us main thread safety as well as cancellation cooperation
private val coroutineContext = context + Dispatchers.Main.immediate
override suspend fun emitSource(source: LiveData<T>): DisposableHandle =
withContext(coroutineContext) {
return@withContext target.emitSource(source)
}
override suspend fun emit(value: T) = withContext(coroutineContext) {
target.clearSource()
target.value = value
}
}
emit() 方法首先会清掉之前添加进来的 source,然后给 LiveData setValue
emitSource() 方法实际调用了 CoroutineLiveData.emitSource() 方法,在内部同样清掉了之前的 source,并利用拓展函数 addDisposableSource 调用父类的 addSource 方法,并返回当前 source 的一层封装 emittedSource,以便在取消前一个 source 时使用。
至此 CoroutineLiveData 也就分析完了。其实这里我们忽略了一个重要角色:MediatorLiveData。CoroutineLiveData、LiveData 的各种 Transformations 其实都是基于 MediatorLiveData 实现的。它的实现也不复杂,但在我们的开发中使用频率很低。感兴趣的同学可以自行学习一下。