协程(14) | 让你的代码支持Flow

6,586 阅读5分钟

前言

协程到现在,我们已经差不多学完了所有基础知识,包括协程启动方式、挂起函数、结构化并发、异常处理、Channel以及Flow等,而关于Flow的进阶使用以及协程更多进阶使用,在后面还需要继续探索。

在之前有一篇文章,我们简单实现了一个Retrofit,并且使用协程的API实现了挂起函数,让我们可以用同步的方式写异步代码。文章地址:# 协程(09) | 实现一个简易Retrofit

那这还不够过瘾,因为我们之前学习Flow的时候,知道Flow就像是一条河流,那假如我们从网络获取的数据,就像是河流一样流淌下来,我们使用各种中间操作符进行处理,最后再展示出来,使用链式调用,不仅大大简化代码编写,还让逻辑更加清晰。

本章内容我们就来实现一个简易的支持Flow返回类型的Retrofit。和支持挂起函数一样,我们分为2个方向:第一个方向是不改动原来SDK代码,把Callback类型改成支持Flow,这种适合我们没有第三方库源码的情况;第二个方向是直接有权限修改源码,在源码阶段支持Flow

正文

代码实现还是继续第9篇中的简易Retrofit代码,所以这里简易先看之前的文章。和实现挂起函数一样,我们先来改造Callback

CallbackFlow

和实现挂起函数一样,我们给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中,比如上面代码块中的trySendBlockingclose方法,这也就说明该方法实现是使用了Channel

由于该方法返回的Flow是冷流,只有当一个终端操作符被调用时,该block才会执行。

  • 该构造者builder能确保线程安全和上下文保存,因此提供的ProducerScope可以在任何上下文中使用,比如基于Callback的API中,也就是本例测试代码中。

结果Flow会在block代码块执行完立即结束,所以应该调用awaitClose挂起函数来保证flow在运行,否则channel会在block执行完成立即close,这也就是为什么在上面代码中,写完业务代码,还要调用awaitClose挂起函数的原因。

  • awaitClose是一个高阶函数,它的参数block会在Flow的消费者手动取消Flow的收集,或者基于Callback的API中调用SendChannelclose方法时被执行。

所以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

打印如下:

image.png

在红框中,代码执行在主线程,网络请求部分执行在工作线程,这样就完成了异步请求,也不会造成Android的UI卡顿了。

总结

本篇文章从2个方面来介绍了Flow的使用,当我们使用第三方库时,可以使用第一种方法来支持Flow;当是新代码时,我们就可以直接让其支持Flow

本篇文章涉及的代码:github.com/horizon1234…