End

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>,所以之前定义的 KtCallCallback 其实已经不需要了。但是,为了之前版本的代码风格保持一致,这里还是保留了 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

posted @ 2018-06-09 20:59  白乾涛  阅读(2553)  评论(0编辑  收藏  举报