前言
好久没有写文章了,最近闲下来了,就把我自己封装的网络请求框架分享一下,自己的项目中也在用
从Java
切换到Kotlin
的开发应该都能感觉到Kotlin
语法糖是真的香,以前使用Java
的时候请求框架一般都是用的RxJava
,添加RxJava2CallAdapterFactory
用Observable
接收返回结果,开发者不需要在做其他操作就可以愉快的使用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 Client
和Retrofit
实例的代码了,不会的百度吧。
请求方法其实就是一个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()
通过日志我们也能发现,协程是基于线程池封装的上层Api
,看2-5行复用的两个线程,但是启动的是不同的协程,这里也算是一道面试题吧,面试的时候别再说协程是轻量级线程,比线程性能好等等话术了。
可以这样说,协程是基于线程池封装的上层Api,结合kotlin语言特性,可以用同步代码的方式,去执行异步操作的一种上层框架。
Kotlin
和java
都是在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)
}
}
}
}
基础不太好的同学可能看不懂inline
和crossinline
,建议看一下扔物线大佬的视频,里面还有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
})
}
请求结果:
就上面简单几行代码就可以实现网络请求,有没有被惊艳到,哈哈哈··· ,其实不过如此;
至于生命周期的问题,这个不用担心,因为有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
真的香。