Kotlin协程+Retrofit网络请求如此简单

2,218 阅读9分钟

前言

好久没有写文章了,最近闲下来了,就把我自己封装的网络请求框架分享一下,自己的项目中也在用

Java切换到Kotlin的开发应该都能感觉到Kotlin语法糖是真的香,以前使用Java的时候请求框架一般都是用的RxJava,添加RxJava2CallAdapterFactoryObservable接收返回结果,开发者不需要在做其他操作就可以愉快的使用RxJava的各种操作符了。

@POST("xxxxx")
Observable<XXXXX> post(@Body RequestBody body);

没有使用过RxJava的可以不用学了,建议直接去学习Kotlin Flow,以后Kotlin开发是趋势了Google现在也在大力推Kotlin

好了,废话不多说了,下面正式开始介绍下自己封装的请求框架,写的不好的地方轻点喷 ^-^

1.BaseResponse

相信大家服务器返回的Json结构都是类似下面这种的

{
    "data": {},
    "code": 0,
    "msg": ""
}

所以我们也要简单封装一下,比较简单,我就直接贴代码了

/**
 * 1.如果需要框架帮你处理服务器状态码请继承它!!
 * 2.必须实现抽象方法,根据自己的业务判断返回请求结果是否成功
 */
abstract class BaseResponse<T> {

    /**
     * 需重写改方法,比如后台code的1000=成功,return code=1000
     */
    abstract fun isSuccess(): Boolean

    /**
     * 获取后台的状态码
     */
    abstract fun getResponseCode(): Int

    /**
     * 获取后台的msg
     */
    abstract fun getResponseMsg(): String

    /**
     * 请求成功后真正关心的数据
     */
    abstract fun getResponseData(): T?

}

假如后台规则不统一,有些地方是code=200代表成功,有些接口是code=0代表成功,这种最好让后端统一一下,实在统一不了了,可以继承BaseResponse,重写getResponseCode()返回该接口的成功的状态即可,如下代码code=0的情况。

这里的状态码不是Http的状态码注意区分下。

/**
 * 1.继承 BaseResponse
 * 2.errorCode, errorMsg,  data 要根据自己服务的返回的字段来定
 * 3.重写isSucces 方法,编写你的业务需求,根据自己的条件判断数据是否请求成功
 * 4.重写 getResponseCode、getResponseData、getResponseMsg方法,传入你的 code data msg
 */
data class ApiCodeResponse<T>(var code: Int, var msg: String, var data: T?) : BaseResponse<T>() {


    override fun getResponseCode() = code

    override fun getResponseData() = data

    override fun getResponseMsg() = msg

    override fun isSuccess(): Boolean = code == 0

}

2.请求方法

这里不贴创建OkHttp ClientRetrofit实例的代码了,不会的百度吧。

请求方法其实就是一个Top-level + CoroutineScope的扩展函数

Kotlin协程熟练的应该都知道,协程必须在作用域(CoroutineScope)内才能launch{}

Android JetPack组件中大部分都提供了生命周期绑定的作用域,例如

//1.在Activity中,不管调用方是主线程还是子线程,launch{}内都在主线程
lifecycleScope.launch{}

//2.在Fragment中,同上在主线程
lifecycleScope.launch{}
//或者想要跟Root View的生命周期绑定的话,同上在主线程
viewLifecycleOwner.lifecycleScope.launch {  }

//3.在ViewModule中,同上在主线程
viewModelScope.launch{}

//如果不在上述三个地方可以使用下面两种

//4.可以理解为一次性的,在指定的线程执行,没有指定就在默认的线程,非主线程
CoroutineScope(context).launch {}

//5.也可以使用全局的,生命周期是Application的,尽量少用这种,除非需求必须用到
//默认在非主线程,可以指定线程运行
 GlobalScope.launch {  }

关于默认所在的线程我们写代码验证下

val ex = CoroutineExceptionHandler { _, throwable ->
    throwable.printStackTrace()
}

lifecycleScope.launch {
    XLogUtils.v("joker launch1=${Thread.currentThread().name}")
}
GlobalScope.launch {
    XLogUtils.d("joker launch2=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Main) {
    XLogUtils.d("joker launch2-1=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.IO) {
    XLogUtils.v("joker launch2-2=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Default) {
    XLogUtils.i("joker launch2-3=${Thread.currentThread().name}")
}
CoroutineScope(Dispatchers.Default).launch {
    XLogUtils.v("joker launch3=${Thread.currentThread().name}")
}
CoroutineScope(Dispatchers.Main).launch {
    XLogUtils.d("joker launch3-1=${Thread.currentThread().name}")
}
CoroutineScope(ex).launch {
    XLogUtils.d("joker launch3-2=${Thread.currentThread().name}")
}
Thread {
    XLogUtils.e("joker launch4=${Thread.currentThread().name}")
    lifecycleScope.launch {
        XLogUtils.i("joker launch5=${Thread.currentThread().name}")
    }
}.start()

image.png

通过日志我们也能发现,协程是基于线程池封装的上层Api,看2-5行复用的两个线程,但是启动的是不同的协程,这里也算是一道面试题吧,面试的时候别再说协程是轻量级线程,比线程性能好等等话术了。

可以这样说,协程是基于线程池封装的上层Api,结合kotlin语言特性,可以用同步代码的方式,去执行异步操作的一种上层框架。

Kotlinjava都是在JVM虚拟机上面运行,难不成Kotlin还能自己搞一套线程???

我们看看lifecycleScope的实现

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate//这里指定了默认线程
            )
            if (internalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

上面代码看不懂要去好好补充一下协程和Android JetPack的知识点哦,本文不在这里叙述了,默认大家都熟练了。

有点跑题了,下面直接贴核心代码了

2.1 直接解析成想要的Json Bean

这里用的WanAndroid开放Api做测试,这里也需要读者具备Retrofit的基础,不懂的还是去学习下。

Retrofit在2.6版本支持使用suspend关键字就可以返回data

/**
 * 获取首页文章数据
 */
@GET("article/list/{page}/json")
suspend fun getArticleList(@Path("page") pageNo: Int): ApiCodeResponse<ApiListResponse<WanAndroidBean>>

可以对分页类型的Json再次封装,不是本文重点这里我就直接贴代码了

data class ApiListResponse<T>(
        var datas: ArrayList<T>,
        var curPage: Int,
        var offset: Int,
        var over: Boolean,
        var pageCount: Int,
        var size: Int,
        var total: Int
) {

    /**
     * 是否是空数据
     */
    fun isEmpty() = datas.isNullOrEmpty()

    /**
     * 是否有更多
     */
    fun isLoadMore() = (curPage * size) < total

}

WanAndroidBean只取了其中两个字段

data class WanAndroidBean(
    var author: String? = "",
    var title: String? = ""
)

上面代码都没什么核心,就是根据实际的接口Json封装BaseBean,下面就看核心发送请求和解析data的代码,其实也很简单,前提是有Kotlin语法、协程的基础,代码如下


/**
 * 发送请求并过滤服务器code,只取成功的data不为空的数据,失败提示服务器errorMsg
 *
 * @param block 网络请求的方法块
 * @param success 成功回调  返回服务器data对象,也就是泛型{@ T}
 * @param error 失败回调    
 */
inline fun <T> CoroutineScope.request22(
    crossinline block: suspend () -> ApiCodeResponse<T>,
    crossinline success: (T) -> Unit,
    crossinline error: (code: Int, errorMsg: String?) -> Unit = { _, _ -> }
): Job {
    return launch {
        try {
            //切换到IO线程执行网络请求
            val response = withContext(Dispatchers.IO) { block() }
            //判断服务器状态码和data不能为空,切换主线程回调回去
            withContext(Dispatchers.Main){
                if (response.isSuccess() && response.data != null) {
                    success(response.data!!)
                } else {
                    error(response.code, "data is null")
                }   
            }
        } catch (e: Exception) {
            e.printStackTrace()
            //异常情况,需要单独处理,一般需要再主线程中处理
            withContext(Dispatchers.Main) {
                error(-1, e.message)
            }
        }
    }
}

基础不太好的同学可能看不懂inlinecrossinline,建议看一下扔物线大佬的视频,里面还有Kotlin相关的视频都可以看看,刚开始我也是看他的视频。

上面代码是CoroutineScope的扩展方法,也就在协程作用域内可以直接调用,上面讲了常用的5种,下面就直接看下再Activity中如何发送请求吧

//不关心失败的情况
mBinding.request.setOnClickListener {
    lifecycleScope.request22({ homeApi.getArticleList(0) }, {
        //接口调用成功 do something
        LogUtils.v("data=$it")
    })
}

//需要处理失败的情况
mBinding.request.setOnClickListener {
    lifecycleScope.request22({ homeApi.getArticleList(0) }, {
        //接口调用成功 do something
        XLogUtils.v("data=$it")
    },{ code, errorMsg ->
        //接口调用失败 do something
    })
}

请求结果:

image.png

就上面简单几行代码就可以实现网络请求,有没有被惊艳到,哈哈哈··· ,其实不过如此;

至于生命周期的问题,这个不用担心,因为有CoroutineScope,也可以把return的Job调用其cancel()

2.2 需要判断Http的code

2.1可以直接把请求过程中的json string流直接转化成客户端使用的bean字典,这要归功于Retrofit内部做的处理,但是有时候业务需求需要知道Http的状态码,比如403鉴权失败,只需要对上述代码稍微改动一下就行了

返回的类型变了

    /**
     * 获取首页文章数据
     */
    @GET("article/list/{page}/json")
    suspend fun getArticleList(@Path("page") pageNo: Int): Response<ApiCodeResponse<ApiListResponse<WanAndroidBean>>>
/**
 * 发送请求并过滤服务器code,只取成功的data不为空的数据,失败提示服务器errorMsg
 *
 * @param block 网络请求的方法块
 * @param success 成功回调  返回服务器data对象,也就是泛型{@ T}
 * @param error 失败回调    
 */
inline fun <T> CoroutineScope.request33(
    crossinline block: suspend () -> Response<ApiCodeResponse<T>>,//这里变了
    crossinline success: (T) -> Unit,
    crossinline error: (code: Int, errorMsg: String?) -> Unit = { _, _ -> }
): Job {
    return launch {
        try {
            //切换到IO线程执行网络请求
            val response = withContext(Dispatchers.IO) { block() }

            //下面代码变了
            withContext(Dispatchers.Main) {
                //判断Http的code
                if (response.isSuccessful && response.code() == 200) {
                    val data = response.body()
                    //判断服务器状态码和data不能为空,切换主线程回调回去
                    if (data?.isSuccess() == true && data.data != null) {
                        success(data.data!!)
                    }
                } else {
                    error(response.code(), "data is null")
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            //异常情况,需要单独处理,一般需要再主线程中处理
            withContext(Dispatchers.Main) {
                error(-1, e.message)
            }
        }
    }
}

其实这里没什么说的,这属于Retrofit的基础,当然也可以直接把json string返回自己解析,这些都是可以实现的

/**
 * 获取首页文章数据
 */
@GET("article/list/{page}/json")
suspend fun getArticleList(@Path("page") pageNo: Int): ResponseBody

注意上面用ResponseBody接收的

/**
 * 发送请求,返回string,自行解析,
 *
 * @param block 网络请求的方法块
 * @param success 成功回调  返回服务器data对象,也就是泛型{@ T}
 * @param error 失败回调   
 */
inline fun CoroutineScope.requestString(
    crossinline block: suspend () -> ResponseBody,
    crossinline success: (String?) -> Unit,
    crossinline error: (code: Int, errorMsg: String?) -> Unit = { _, _ -> },
): Job {
    return launch {
        try {
            val result = withContext(Dispatchers.IO) {
                val responseBody = block()
                //直接拿string 流想咋样解析都可以做到
                responseBody.string()
            }
            withContext(Dispatchers.Main) { success(result) }
        } catch (e: Exception) {
            e.printStackTrace()
            withContext(Dispatchers.Main) {
                error(-1, e.message)
            }
        }
    }
}

3.总结

本文只提供一种封装思路,真的在项目中使用不可能这么简单,像不同服务器状态码弹不同Toas,分页处理等等的,但是需要熟练掌握Kotlin、协程、OkHttp、Retrofit、JetPack组件等等的使用方法,想怎么封装你来定。

当然也可以结合Flow封装,使用Flow操作符都是可以的

本文代码只贴了核心部分,源码可以提供部分,因为在项目中好多代码和项目的业务绑定的,不太好拆出来, 能看懂上面的扩展函数的应该大概都清楚了。

也可以参考很久之前写的,大差不差,思想就是用扩展函数简化发起请求的代码。点这里

Google在慢慢统一Androd开发者的编程风格,JetPack库、AndroidX各种库、Flow、协程等等;再看看5年前真的百家齐放,各种请求框架,现在还清楚记得以前很火的Volley,也有基于RxJava在次封装的。使用协程后个人感觉如果项目没什么特别的需求就不用做过多的封装,使用扩展函数简化掉接口的代码量就行了。

在这里建议大家还没在项目中用到Kotlin的赶紧用起来。Kotlin真的香。