前语

作为Android开发,大名鼎鼎的Retrofit网络恳求库肯定都用过,并且在Kotlin更新协程后,Retrofit也榜首时间更新了协程办法、Flow办法等编码模式,这篇文章咱们运用前面的学习常识,测验着实现一个主张版本的Retrofit,然后看看怎么运用挂起函数,来以同步的办法实现异步的代码

正文

Retrofit触及的常识点仍是蛮多的,包含自界说注解、动态署理、反射等常识点,咱们就来温习一下,最后再看怎么运用协程来把咱们不喜欢的Callback给消灭掉。

界说注解

Retrofit相同,咱们界说俩个注解:

/**
* [Field]注解用在API接口界说的办法的参数上
* */
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Field(val value: String)
/**
 * [GET]注解用于标记该办法的调用是HTTP的GET办法
 * */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val value: String)

这儿咱们界说俩个注解,Field用来给办法参数设置,GET用来给办法设置,表明它是一个HTTPGET办法。

界说ApiService

Retrofit相同,来界说一个接口文档,里面界说咱们需求运用的接口:

/**
 * [ApiService]类界说了整个项目需求调用的接口
 * */
interface ApiService{
    /**
     * [reposAsync]用于异步获取库房信息
     *
     * @param language 要查询的语言,http实在调用的是[Field]中的lang
     * @param since 要查询的周期
     *
     * @return
     * */
    @GET("/repo")
    fun reposAsync(
        @Field("lang") language: String,
        @Field("since") since: String
    ): KtCall<RepoList>
    /**
     * [reposSync]用于同步调用
     * @see [reposSync]
     * */
    @GET("/repo")
    fun reposSync(
        @Field("lang") language: String,
        @Field("since") since: String
    ): RepoList
}

这儿咱们查询GitHub上某种语言近期的热门项目,其间reposAsync表明异步调用,回来值类型是KtCall<RepoList>,而reposSync表明同步调用,这儿触及的RepoList便是回来值的数据类型:

data class RepoList(
    var count: Int?,
    var items: List<Repo>?,
    var msg: String?
)
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?
)

KtCall则是用来承载异步调用的回调简略处理:

/**
 * 该类用于异步恳求承载,首要是用来把[OkHttp]中回来的恳求值给转化
 * 一下
 *
 * @param call [OkHttp]结构中的[Call],用来进行网络恳求
 * @param gson [Gson]的实例,用来反序列化
 * @param type [Type]类型实例,用来反序列化
 * */
class KtCall<T: Any>(
    private val call: Call,
    private val gson: Gson,
    private val type: Type
){
    fun call(callback: CallBack<T>): Call{
        call.enqueue(object : okhttp3.Callback{
            override fun onFailure(call: Call, e: IOException) {
                callback.onFail(e)
            }
            override fun onResponse(call: Call, response: Response) {
                try {
                    val data = gson.fromJson<T>(response.body?.string(),type)
                    callback.onSuccess(data)
                }catch (e: java.lang.Exception){
                    callback.onFail(e)
                }
            }
        })
        return call
    }
}

在这儿界说了一个泛型类,用来处理T类型的数据,异步调用仍是调用OkHttpCallenqueue办法,在其间对OkHttpCallback进行封装和处理,转变为咱们界说的Callback类型:

/**
 * 事务运用的接口,表明回来的数据
 * */
interface CallBack<T: Any>{
    fun onSuccess(data: T)
    fun onFail(throwable:Throwable)
}

这儿咱们暂时只简略抽象为成功和失利。

单例Http工具类

再接着,咱们模仿Retrofit,来运用动态署理等技术来进行处理:

/**
 * 单例类
 *
 * */
object KtHttp{
    private val okHttpClient = OkHttpClient
        .Builder()
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BASIC
        })
        .build()
    private val gson = Gson()
    val baseUrl = "https://trendings.herokuapp.com"
    /**
     * 运用Java的动态署理,传递[T]类型[Class]目标,能够回来[T]的
     * 目标。
     * 其间在lambda中,一共有3个参数,当调用[T]目标的办法时,会动态
     * 署理到该lambda中履行。[method]便是目标中的办法,[args]是该
     * 办法的参数。
     * */
    fun <T: Any> create(service: Class<T>): T {
        return Proxy.newProxyInstance(
            service.classLoader,
            arrayOf(service)
        ){ _,method,args ->
            val annotations = method.annotations
            for (annotation in annotations){
                if (annotation is GET){
                    val url = baseUrl + annotation.value
                    return@newProxyInstance invoke<T>(url, method, args!!)
                }
            }
            return@newProxyInstance null
        } as T
    }
    /**
     * 调用[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
        var url = path
        val paramAnnotations = method.parameterAnnotations
        for (i in paramAnnotations.indices){
            for (paramAnnotation in paramAnnotations[i]){
                if (paramAnnotation is Field){
                    val key = paramAnnotation.value
                    val value = args[i].toString()
                    if (!url.contains("?")){
                        url += "?$key=$value"
                    }else{
                        url += "&$key=$value"
                    }
                }
            }
        }
        val request = Request.Builder()
            .url(url)
            .build()
        val call = okHttpClient.newCall(request)
        //泛型判别
        return if (isKtCallReturn(method)){
            val genericReturnType = getTypeArgument(method)
            KtCall<T>(call, gson, genericReturnType)
        } 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)
        }
    }
    /**
     * 判别办法回来类型是否是[KtCall]类型。这儿调用了[Gson]中的办法。
    */
    private fun isKtCallReturn(method: Method) =
        getRawType(method.genericReturnType) == KtCall::class.java
    /**
     * 获取[Method]的回来值类型中的泛型参数
     * */
    private fun getTypeArgument(method: Method) =
        (method.genericReturnType as ParameterizedType).actualTypeArguments[0]
}

上面的代码首要分为俩个部分,榜首部分运用Java的动态署理类Porxy,能够通过create办法创立一个接口目标。调用该接口目标的办法,会被署理到lambda中进行处理,在lambda中咱们对有GET润饰的办法进行额外处理。

第二部分便是办法的拼接和调用处理,先是针对Field注解润饰的办法参数,给拼接到url中,然后便是重点地方,判别办法的回来值类型,是否是KtCall类型,如果是的话,就以为是异步调用,不然便是同步调用。

关于异步调用,咱们封装为一个KtCall的目标,而关于同步调用,咱们能够直接运用Gson来解分出咱们希望的数据。

Android客户端测验

这样咱们就完结了一个简易的既有同步又有异步调用的网络恳求封装库,咱们写个页面调用一下如下:

//同步调用
private fun sync(){
    thread {
        val apiService: ApiService = KtHttp.create(ApiService::class.java)
        val data = apiService.reposSync(language = "Kotlin", since = "weekly")
        runOnUiThread {
            findViewById<TextView>(R.id.result).text = data.toString()
            Toast.makeText(this, "$data", Toast.LENGTH_SHORT).show()
        }
    }
}
//异步调用
private fun async(){
    KtHttp.create(ApiService::class.java).reposAsync(language = "Java", since = "weekly").call(object : CallBack<RepoList>{
        override fun onSuccess(data: RepoList) {
            runOnUiThread {
                findViewById<TextView>(R.id.result).text = data.toString()
                Toast.makeText(this@MainActivity, "$data", Toast.LENGTH_SHORT).show()
            }
        }
        override fun onFail(throwable: Throwable) {
            runOnUiThread {
                findViewById<TextView>(R.id.result).text = throwable.toString()
                Toast.makeText(this@MainActivity, "$throwable", Toast.LENGTH_SHORT).show()
            }
        }
    })
}

经过测验,这儿代码能够正常履行。

协程小试牛刀

在前面咱们说过挂起函数能够用同步的代码来写出异步的作用,就比如这儿的异步回调,咱们能够运用协程来进行简略改造。

首先,想把Callback类型的办法改成挂起函数办法的,有2种办法。榜首种是不改变本来代码库的办法,在Callback上面套一层,也是本篇文章所介绍的办法。第二种是修改本来代码块的源码,运用协程的底层API,这个办法等后边再说。

其实在本来Callback上套一层非常简略,咱们只需求运用协程库为咱们供给的2个顶层函数即可:

/**
 * 把本来的[CallBack]办法的代码,改成协程样式的,即消除回调,运用挂起函数来完结,以同步的办法来
 * 完结异步的代码调用。
 *
 * 这儿的[suspendCancellableCoroutine] 翻译过来便是挂起可撤销的协程,因为咱们需求结果,所以
 * 需求在适宜的机遇康复,而康复便是通过[Continuation]的[resumeWith]办法来完结。
 * */
suspend fun <T: Any> KtCall<T>.await() : T =
    suspendCancellableCoroutine { continuation ->
        //开始网络恳求
        val c = call(object : CallBack<T>{
            override fun onSuccess(data: T) {
                //这儿扩展函数也是奇葩,容易重名
                continuation.resume(data)
            }
            override fun onFail(throwable: Throwable) {
                continuation.resumeWithException(throwable)
            }
        })
        //当收到cancel信号时
        continuation.invokeOnCancellation {
            c.cancel()
        }
}

这儿咱们引荐运用suspendCancelableCoroutine高阶函数,听名字翻译便是挂起可撤销的协程,咱们给KtCall扩展一个挂起办法await,在该办法中,咱们运用continuation目标来处理康复的值,同时还能够响应撤销,来撤销OkHttp的调用。

这儿留意的便是resume运用的是扩展函数,与之类似的还有一个suspendCoroutine办法,这个办法无法响应撤销,咱们不主张运用。

在界说完上面代码后,咱们在Android运用一下:

findViewById<TextView>(R.id.coroutineCall).setOnClickListener {
    lifecycleScope.launch {
        val data = KtHttp.create(ApiService::class.java).reposAsync(language = "Kotlin", since = "weekly").await()
        findViewById<TextView>(R.id.result).text = data.toString()
    }
}

能够发现在这种情况下,咱们就能够运用同步的办法写出了异步代码,因为挂起函数的特性,下面那行UI操作会等到挂起函数康复后才会履行。

总结

本篇文章首要是介绍了一些常用常识点,也让咱们对Retrofit的各种办法回来类型兼容性有了必定了解,最后咱们运用了在不改变本来代码库的情况下,运用封装一层的办法,来实现以同步的代码写异步的办法。

本篇文章代码地址: github.com/horizon1234…