前言
协程到现在,我们已经差不多学完了所有基础知识,包括协程启动方式、挂起函数、结构化并发、异常处理、Channel
以及Flow
等,而关于Flow
的进阶使用以及协程更多进阶使用,在后面还需要继续探索。
在之前有一篇文章,我们简单实现了一个Retrofit
,并且使用协程的API实现了挂起函数,让我们可以用同步的方式写异步代码。文章地址:# 协程(09) | 实现一个简易Retrofit。
那这还不够过瘾,因为我们之前学习Flow
的时候,知道Flow
就像是一条河流,那假如我们从网络获取的数据,就像是河流一样流淌下来,我们使用各种中间操作符进行处理,最后再展示出来,使用链式调用,不仅大大简化代码编写,还让逻辑更加清晰。
本章内容我们就来实现一个简易的支持Flow
返回类型的Retrofit
。和支持挂起函数一样,我们分为2个方向:第一个方向是不改动原来SDK代码,把Callback
类型改成支持Flow
,这种适合我们没有第三方库源码的情况;第二个方向是直接有权限修改源码,在源码阶段支持Flow
。
正文
代码实现还是继续第9篇中的简易Retrofit
代码,所以这里简易先看之前的文章。和实现挂起函数一样,我们先来改造Callback
。
Callback
转Flow
和实现挂起函数一样,我们给KtCall
类型再加一个扩展函数asFlow
:
/**
* 把原来[CallBack]形式的代码,改成[Flow]样式的,即消除回调。其实和扩展挂起函数一样,大致有如下步骤:
* * 调用一个高阶函数,对于成功数据进行返回,即[trySendBlocking]方法
* * 对于失败的数据进行返回异常,即[close]方法
* * 同时要可以响应取消,即[awaitClose]方法
* */
fun <T: Any> KtCall<T>.asFlow(): Flow<T> =
callbackFlow {
//开始网络请求
val c = call(object : CallBack<T>{
override fun onSuccess(data: T) {
//返回正确的数据,但是要调用close()
trySendBlocking(data)
.onSuccess { close() }
.onFailure { close(it) }
}
override fun onFail(throwable: Throwable) {
//返回异常信息
close(throwable)
}
})
awaitClose {
//响应外部取消请求
c.cancel()
}
}
这里的代码比较简单,但是有许多细节知识点,我们来简单分析一下:
- 通过
callbackFlow
高阶函数实现功能,返回Flow
类型的数据,该函数定义:
public fun <T> callbackFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit):
Flow<T> = CallbackFlowBuilder(block)
该方法通过ProducerScope
,向block
代码块中提供SendChannel
实例,通过SendChannel
实例,我们可以向其中发射元素,从而创建出一个冷的Flow
。
这个函数的定义,在之前文章中我们反复强调过,block
是高阶函数类型,它的接收者是ProducerScope
:
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
public val channel: SendChannel<E>
}
通过该接口中的默认属性channel
,我们可以发送数据到Channel
中,比如上面代码块中的trySendBlocking
和close
方法,这也就说明该方法实现是使用了Channel
。
由于该方法返回的Flow
是冷流,只有当一个终端操作符被调用时,该block
才会执行。
- 该构造者
builder
能确保线程安全和上下文保存,因此提供的ProducerScope
可以在任何上下文中使用,比如基于Callback
的API中,也就是本例测试代码中。
结果Flow
会在block
代码块执行完立即结束,所以应该调用awaitClose
挂起函数来保证flow
在运行,否则channel
会在block
执行完成立即close
,这也就是为什么在上面代码中,写完业务代码,还要调用awaitClose
挂起函数的原因。
awaitClose
是一个高阶函数,它的参数block
会在Flow
的消费者手动取消Flow
的收集,或者基于Callback
的API中调用SendChannel
的close
方法时被执行。
所以awaitClose
可以用来做一些block
完成后的收尾工作,比如上面代码中我们用来取消OkHttp
的请求,或者在反注册一些Callback
。
同时awaitClose
是必须要调用的,可以防止当flow
被取消时发生内存泄漏,否则代码会一直执行,即使flow
的收集已经完成了。
为了杜绝上面情况,我们在Callback
中,如果业务代码执行完成,不论是成功还是失败,都需要调用close
,就比如上面代码中返回成功和返回失败都要调用close
,并且在失败时,还需要传递参数。
写完上面代码,我们也做了一个简单分析,主要是一些规则要执行,现在我们就来在代码中使用一下:
findViewById<TextView>(R.id.flowCall).setOnClickListener {
val dataFlow = KtHttp.create(ApiService::class.java).reposAsync(language = "Kotlin", since = "weekly").asFlow()
dataFlow
.onStart {
Toast.makeText(this@MainActivity, "开始请求", Toast.LENGTH_SHORT).show()
}
.onCompletion {
Toast.makeText(this@MainActivity, "请求完成", Toast.LENGTH_SHORT).show()
}
.onEach {
findViewById<TextView>(R.id.result).text = it.toString()
}
.catch {
Log.i("Flow", "catch exception: $it")
}
.launchIn(lifecycleScope)
}
现在我们的网络请求返回值就变成了Flow
类型,我们就可以使用Flow
的API进行链式调用,在编码和逻辑上都更加方便。
直接支持Flow
上面代码使用Callback
转为Flow
适用于一些第三方库,我们无权修改源码,但是大多数情况下,我们还是可以修改源码的。
就比如本章所说的简易Retrofit
,没看过之前的代码实现还是建议看一下,这里我们根据之前实现异步效果一样,来定义一个直接返回Flow
类型的方法:
/**
* [reposFlow]用于异步调用,同时返回类型是[Flow]
* */
@GET("/repo")
fun reposFlow(
@Field("lang") language: String,
@Field("since") since: String
): Flow<RepoList>
然后还是判断方法的返回值,类似于之前判断返回值类型是否是KtCall
一样,我们判断返回值是否是Flow
类型:
/**
* 判断方法返回值类型是否是[Flow]类型
* */
private fun isFlowReturn(method: Method) =
getRawType(method.genericReturnType) == Flow::class.java
然后在具体调用的invoke
方法中进行处理:
/**
* 调用[OkHttp]功能进行网络请求,这里根据方法的返回值类型选择不同的策略。
* @param path 这个是HTTP请求的url
* @param method 定义在[ApiService]中的方法,在里面实现中,假如方法的返回值类型是[KtCall]带
* 泛型参数的类型,则认为需要进行异步调用,进行封装,让调用者传入[CallBack]。假如返回类型是普通的
* 类型,则直接进行同步调用。
* @param args 方法的参数。
* */
private fun <T: Any> invoke(path: String, method: Method, args: Array<Any>): Any?{
if (method.parameterAnnotations.size != args.size) return null
...
//泛型判断
return when{
isKtCallReturn(method) -> {
val genericReturnType = getTypeArgument(method)
KtCall<T>(call, gson, genericReturnType)
}
isFlowReturn(method) -> {
logX("Start Out")
flow<T> {
logX("Start In")
val genericReturnType = getTypeArgument(method)
val response = okHttpClient.newCall(request).execute()
val json = response.body?.string()
val result = gson.fromJson<T>(json, genericReturnType)
// 传出结果
logX("Start Emit")
emit(result)
logX("End Emit")
}
}
else -> {
val response = okHttpClient.newCall(request).execute()
val genericReturnType = method.genericReturnType
val json = response.body?.string()
Log.i("zyh", "invoke: json = $json")
//这里这个调用,必须要传入泛型参数
gson.fromJson<Any?>(json, genericReturnType)
}
}
}
在isFlowReturn
分支中,我们首先加了一些可以打印协程信息的log,方便我们看线程切换效果。然后就是我们非常熟悉的flow{}
高阶函数,它是Flow
的上游操作符,在创建Flow
的同时,使用emit
发送数据,这部分知识点在Flow
的文章中,我们已经非常熟悉了。
最后我们来进行调用:
findViewById<TextView>(R.id.flowReturnCall).setOnClickListener {
KtHttp.create(ApiService::class.java).reposFlow(language = "Kotlin", since = "weekly")
.flowOn(Dispatchers.IO)
.onStart {
Toast.makeText(this@MainActivity, "开始请求", Toast.LENGTH_SHORT).show()
}
.onCompletion {
Toast.makeText(this@MainActivity, "请求完成", Toast.LENGTH_SHORT).show()
}
.catch {
Log.i("Flow", "catch exception: $it")
}
.onEach {
logX("Display UI")
findViewById<TextView>(R.id.result).text = it.toString()
}
.launchIn(lifecycleScope)
}
同样的,我们使用flowOn
来切换该操作符之前的操作的线程,然后使用launchIn
在收集数据的同时指定Scope
。
打印如下:
在红框中,代码执行在主线程,网络请求部分执行在工作线程,这样就完成了异步请求,也不会造成Android的UI卡顿了。
总结
本篇文章从2个方面来介绍了Flow
的使用,当我们使用第三方库时,可以使用第一种方法来支持Flow
;当是新代码时,我们就可以直接让其支持Flow
。
本篇文章涉及的代码:github.com/horizon1234…