Kotlin 朱涛-24 实战 Retrofit 协程 Flow
目录
24 | 实战:让 KtHttp 支持 Flow
在之前的 4.0 版本中,为了让 KtHttp 支持挂起函数,有两种思路,一种是改造内部,另一种是扩展外部。同理,为了让 KtHttp 支持 Flow,也是这两种思路。
- 5.0 版本,基于 4.0 版本的代码,从 KtHttp 的外部扩展出 Flow 的能力
- 6.0 版本,修改 KtHttp 内部,让它支持 Flow API
Callback 转 Flow
Callback 转挂起函数
在第 18 讲中,我们通过扩展函数,在 KtCall 的基础上扩展了挂起函数
的支持,实现了 KtHttp 的异步 Callback
请求。
Callback 转挂起函数,主要有三个步骤:
- 使用
suspendCancellableCoroutine
执行异步 Callback 的代码,即调用call()
方法 - 将异步 Callback 的回调结果传出去,
onSuccess
就传正确的结果,onFail
就传异常信息 - 响应协程取消事件
invokeOnCancellation{}
,在协程被取消后,将 OkHttp 的 call 取消
suspend fun <T : Any> KtCall<T>.await(): T = // 扩展一个 带返回值的挂起函数
suspendCancellableCoroutine { c: CancellableContinuation<T> -> // 支持取消
call(object : Callback<T> { // 调用 call,传入一个 Callback
override fun onSuccess(data: T) = c.resume(data)
override fun onFail(t: Throwable) = c.resumeWithException(t)
})
c.invokeOnCancellation {
println("协程被取消啦")
this.call.cancel() // 将 OkHttp 的 call 取消
}
}
callbackFlow
可使用 callbackFlow
将 Callback 转为 Flow,也是类似的三个步骤:
fun <T : Any> KtCall<T>.asFlow(): Flow<T> = // 扩展一个返回值为 Flow 的普通函数(非挂起函数)
callbackFlow { // 使用 callbackFlow 可将 Callback 转为 Flow
call(object : Callback<T> { // 第一步:调用 call,传入一个 Callback
override fun onSuccess(data: T) { // 第二步:将异步 Callback 的回调结果传出去
val r = trySend(data) // 返回值代表执行结果是成功还是失败
println("trySend: ${r.isSuccess} ${r.isFailure} ${r.isClosed}") // true false false
}
override fun onFail(throwable: Throwable) {
val result: Boolean = close(throwable)
println("close: $result")
}
})
awaitClose { // 第三步:响应协程取消事件,在协程被取消后,将 OkHttp 的 call 取消
println("awaitClose")
call.cancel()
}
}
callbackFlow 的底层用到了 Channel,所以才可以使用 trySend/close
这样的 API。
trySend()
其实就是 send()
的非挂起函数版本的 API。因为 onSuccess/onFail
中没有协程作用域,所以不能直接使用 Channel 的挂起函数 send()
。
awaitClose
上面代码中,如果去掉 awaitClose
,会异常:
Catch: java.lang.IllegalStateException: 'awaitClose { yourCallbackOrListener.cancel() }' should be used in the end of callbackFlow block.
Otherwise, a callback/listener may leak in case of external cancellation.
See callbackFlow API documentation for the details.
去掉后,如果增加
delay(8000L)
也是可以的,当然,这个 delay 时间必须足够大才行。
改用 trySendBlocking
trySend()
不会阻塞,返回值类型是ChannelResult
,代表执行结果是成功还是失败。如果往 Channel 中成功地添加了元素,那么返回值就是成功;如果当前 Channel 已经满了,那么返回值就是失败。trySendBlocking()
会阻塞,它会尽可能发送成功。当 Channel 已满的时候,会阻塞等待,直到管道容量空闲以后再返回成功。
val result: ChannelResult<Unit> = trySend(data) // 非阻塞,Channel 满时会立刻失败
val result: ChannelResult<Unit> = trySendBlocking(data) // 会阻塞,Channel 满时会阻塞等待
主动 close
由于 callbackFlow
的底层是 Channel 实现的,在用完以后,应该主动将其关闭或者释放。不然就会一直占用计算机资源。
trySendBlocking(data) // 需要在发送后的回调中,关闭 Channel
.onSuccess { close() } // 发送成功时关闭
.onFailure { close(it) } // 发送失败时关闭,调用 close 时,注意一定要带上异常信息
cancel(CancellationException("Send channel fail!", t)) // 也可以使用 cancel 方法关闭
注意,在异常情况下调用
close()
时,一定要传入对应的异常参数close(throwable)
,否则,Flow 的下游就无法收到任何的异常信息。
结构化并发
如果在 callbackFlow
中还启动了其他的协程任务,close/cancel
也同样可以取消对应的协程。当然,前提是,不打破它原有的协程父子关系。
fun <T : Any> KtCall<T>.asF3low(): Flow<T> = callbackFlow {
val job = launch {
println("start")
delay(30000L) // 阻止退出
println("end") // 没机会打印
}
job.invokeOnCompletion { println("completed: ${it?.message}") } // 会伴随 Flow 一并取消
// ...
}
上面代码中,由于协程是结构化的,所以,当取消 callbackFlow
的时候,在它内部创建的协程 job
,也会跟着被取消。
使用案例
fun main() = runBlocking {
create(ApiServiceV3::class.java)
.repos(lang = "Kotlin", since = "weekly")
.asFlow()
.catch { println("Catch: $it") }
.collect { println(it) }
}
完整代码
import com.google.gson.Gson
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import okhttp3.*
import java.io.IOException
import java.lang.reflect.*
data class Repo(
var added_stars: String?, var avatars: List<String>?, var desc: String?, var forks: String?,
var lang: String?, var repo: String?, var repo_link: String?, var stars: String?,
)
data class RepoList(var count: Int?, var items: List<Repo>?, var msg: String?)
@Target(AnnotationTarget.FUNCTION) // 修饰函数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class GET(val value: String) // 请求方式
@Target(AnnotationTarget.VALUE_PARAMETER) // 修饰参数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class Field(val value: String) // 请求参数
interface Callback<T : Any> { // 泛型边界 Any 可以保证 T 非空
fun onSuccess(data: T)
fun onFail(throwable: Throwable)
}
class KtCall<T : Any>(
// 泛型边界 Any 可以保证 T 非空
val call: Call, // OkHttp 的 Call 对象,用于发起网络请求(同步或异步)
val type: Type, // Gson 解析时指定的反射类型
) {
fun call(callback: Callback<T>) {
call.enqueue(object : okhttp3.Callback { // 使用 call 异步请求 API
override fun onFailure(call: Call, e: IOException) {
callback.onFail(e) // 根据请求结果,调用 callback 的回调方法
}
override fun onResponse(call: Call, response: Response) {
try {
val text = response.body?.string() // 网络请求结果
val t = Gson().fromJson<T>(text, type) // Gson 解析
callback.onSuccess(t)
} catch (e: Exception) {
callback.onFail(e)
}
}
})
}
}
interface ApiServiceV3 {
@GET("/repo")
fun repos(@Field("lang") lang: String, @Field("since") since: String): KtCall<RepoList>
}
// 改动点 ① :这里对泛型 T 增加了泛型边界 Any 的限制
private fun <T : Any> request(path: String, method: Method, args: Array<Any>): Any? {
val annotations = method.parameterAnnotations
if (annotations.size != args.size) return null
var url = path
for (indice in annotations.indices) {
for (parameterAnnotation in annotations[indice]) {
if (parameterAnnotation is Field) {
val key = parameterAnnotation.value
val value = args[indice].toString()
val param = "$key=$value"
url += if (url.contains("?")) "&$param" else "?$param"
}
}
}
val request = Request.Builder().url(url).build()
// -------------------------- 改动点 ② --------------------------
val call = OkHttpClient().newCall(request) // 创建 OkHttp 的 Call 对象
val pType = method.genericReturnType as ParameterizedType // 带有类型参数的类型
val type = pType.actualTypeArguments[0] // 获取 X<T, P> 里的类型参数的类型(T、P)
return KtCall<T>(call, type) // 例如:KtCall<RepoList> 中的 RepoList 类型
}
// 改动点 ① :这里对泛型 T 增加了泛型边界 Any 的限制
fun <T : Any> create(service: Class<T>): T {
val loader: ClassLoader = service.classLoader
val interfaces = arrayOf<Class<*>>(service)
val any: Any? = Proxy.newProxyInstance(loader, interfaces) { _, method, args ->
for (annotation in method.annotations) {
if (annotation is GET) {
val url = "https://trendings.herokuapp.com${annotation.value}"
return@newProxyInstance request<T>(url, method, args!!) // 改动点 ②
}
}
return@newProxyInstance null
}
@Suppress("UNCHECKED_CAST")
return any as T
}
fun <T : Any> KtCall<T>.asFlow(): Flow<T> = callbackFlow {
call(object : Callback<T> {
override fun onSuccess(data: T) {
trySendBlocking(data)
.onSuccess { close() }
.onFailure { close(it) }
}
override fun onFail(throwable: Throwable) {
close(throwable)
}
})
awaitClose {
println("awaitClose")
call.cancel()
}
}
fun main() = runBlocking {
create(ApiServiceV3::class.java)
.repos(lang = "Kotlin", since = "weekly")
.asFlow()
.catch { println("Catch: $it") }
.collect { println(it) }
}
直接支持 Flow
对于 KtHttp 来说,4.0 版本、5.0 版本都只是外部扩展
,我们对 KtHttp 的内部源码
并没有做改动。
而对于 6.0 版本的开发,我们其实是通过修改内部源码,从而让 KtHttp 可以直接支持返回 Flow 类型的数据。
ApiServiceV6
首先,需要定义一个新的方法,它的返回值类型需要是 Flow<RepoList>
,而不是之前的 KtCall<RepoList>
。这样一来,我们在使用它的时候,就不需要使用扩展函数 asFlow()
了。
interface ApiServiceV6 {
@GET("/repo")
fun reposFlow(@Field("lang") lang: String, @Field("since") since: String): Flow<RepoList>
}
注意:
reposFlow()
方法是一个普通的函数,并不是挂起函数,所以并不需要在协程中调用。
KtCall 和 Callback
由于直接返回了 Flow<RepoList>
,所以之前定义的 KtCall
、Callback
其实已经不需要了。但是,为了之前版本的代码风格保持一致,这里还是保留了 KtCall
。
class KtCall<T : Any>(private val call: Call, private val type: Type) {
fun getFlow(): Flow<T> {
return flow { // 直接返回Flow
val json = call.execute().body?.string() // 同步请求
val result = Gson().fromJson<T>(json, type) // Gson 解析
emit(result) // 传出结果
}
}
}
注意:
flow{}
这个高阶函数,也是一个普通的函数,同样也并不是挂起函数,所以也不需要在协程中调用。
request
request() 方法唯一需要改的,就是最后的返回值,需要有之前的 return KtCall<T>(call, type)
改为 return KtCall<T>(call, type).getFlow()
private fun <T : Any> request(path: String, method: Method, args: Array<Any>): Any? {
// ...
return KtCall<T>(call, type).getFlow() // 返回值类型为 Flow
}
优势与分析
至此,6.0 版本的开发就完成了。对比起 Callback 转 Flow,让 KtHttp 直接支持 Flow 要简单很多。从这一点上,也可以看到 Flow 的强大和易用性。
由于 reposFlow/flow
都是普通函数,而不是挂起函数,就使得 Flow 的易用性非常高。也就是说,对于 Flow 的上游、中间操作符而言,它们其实不需要协程作用域,而只有在下游调用 collect{}
的时候,才需要协程作用域。
可见,正因为 Flow 的上游不需要协程作用域,我们才可以轻松完成 6.0 版本的代码。
使用案例
fun main() { // 不需要 runBlocking
val flow: Flow<RepoList> = create(ApiServiceV6::class.java) // 上游不需要协程作用域
.reposFlow(lang = "Kotlin", since = "weekly") // 中间操作符也不需要协程作用域
.flowOn(Dispatchers.IO)
.catch { println("Catch: $it") }
runBlocking { flow.collect { println(it) } } // 只有在下游调用 collect 时,才需要协程作用域
}
完整代码
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.*
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import java.lang.reflect.*
data class Repo(
var added_stars: String?, var avatars: List<String>?, var desc: String?, var forks: String?,
var lang: String?, var repo: String?, var repo_link: String?, var stars: String?,
)
data class RepoList(var count: Int?, var items: List<Repo>?, var msg: String?)
@Target(AnnotationTarget.FUNCTION) // 修饰函数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class GET(val value: String) // 请求方式
@Target(AnnotationTarget.VALUE_PARAMETER) // 修饰参数
@Retention(AnnotationRetention.RUNTIME) // 运行时可访问 -- 反射的前提
annotation class Field(val value: String) // 请求参数
interface ApiServiceV6 {
@GET("/repo")
fun reposFlow(@Field("lang") lang: String, @Field("since") since: String): Flow<RepoList>
}
private fun <T : Any> request(path: String, method: Method, args: Array<Any>): Any? {
val annotations = method.parameterAnnotations
if (annotations.size != args.size) return null
var url = path
for (indice in annotations.indices) {
for (parameterAnnotation in annotations[indice]) {
if (parameterAnnotation is Field) {
val key = parameterAnnotation.value
val value = args[indice].toString()
val param = "$key=$value"
url += if (url.contains("?")) "&$param" else "?$param"
}
}
}
val request = Request.Builder().url(url).build()
val call = OkHttpClient().newCall(request)
val pType = method.genericReturnType as ParameterizedType
val type = pType.actualTypeArguments[0]
return KtCall<T>(call, type).getFlow() // 返回值类型为 Flow
}
class KtCall<T : Any>(private val call: Call, private val type: Type) {
fun getFlow(): Flow<T> {
return flow { // 直接返回Flow
val json = call.execute().body?.string() // 同步请求
val result = Gson().fromJson<T>(json, type)// Gson 解析
emit(result) // 传出结果
}
}
}
fun <T : Any> create(service: Class<T>): T {
val loader: ClassLoader = service.classLoader
val interfaces = arrayOf<Class<*>>(service)
val any: Any? = Proxy.newProxyInstance(loader, interfaces) { _, method, args ->
for (annotation in method.annotations) {
if (annotation is GET) {
val url = "https://trendings.herokuapp.com${annotation.value}"
return@newProxyInstance request<T>(url, method, args!!)
}
}
return@newProxyInstance null
}
@Suppress("UNCHECKED_CAST")
return any as T
}
fun main() {
val flow: Flow<RepoList> = create(ApiServiceV6::class.java)
.reposFlow(lang = "Kotlin", since = "weekly")
.flowOn(Dispatchers.IO)
.catch { println("Catch: $it") }
runBlocking { flow.collect { println(it) } }
}
2018-06-09
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/9160806.html