首页
解决方案
新闻动态
关于我们
联系我们
教育培训
天安云文档
中烟公司对标BI
天安芯科
文章详情
Kotlin协程+Retrofit网络请求如此简单
2024-09-04
来源:
从Java切换到Kotlin的开发应该都能感觉到Kotlin语法糖是真的香,以前使用Java的时候请求框架一般都是用的RxJava,添加RxJava2CallAdapterFactory 用Observable接收返回结果,开发者不需要在做其他操作就可以愉快的使用RxJava的各种操作符了。
@POST
(
"xxxxx"
)
Observable<XXXXX>
post
(@Body RequestBody body)
;
没有使用过RxJava的可以不用学了,建议直接去学习Kotlin Flow,以后Kotlin开发是趋势了Google现在也在大力推Kotlin。
好了,废话不多说了,下面正式开始介绍下自己封装的请求框架,写的不好的地方轻点喷 ^-^
/ 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
}
/ 请求方法 /
这里不贴创建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的知识点哦,本文不在这里叙述了,默认大家都熟练了。
有点跑题了,下面直接贴核心代码了。
直接解析成想要的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()。
需要判断Http的code
上面可以直接把请求过程中的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)
}
}
}
}
/
总结
/
本文只提供一种封装思路,真的在项目中使用不可能这么简单,像不同服务器状态码弹不同Toas,分页处理等等的,但是需要熟练掌握Kotlin、协程、OkHttp、Retrofit、JetPack组件等等的使用方法,想怎么封装你来定。
当然也可以结合Flow封装,使用Flow操作符都是可以的。
本文代码只贴了核心部分,源码可以提供部分,
因为在项目中好多代码和项目的业务绑定的,不太好拆出来,
能看懂上面的扩展函数的应该大概都清楚了。
也可以参考很久之前写的,大差不差,思想就是用扩展函数简化发起请求的代码。
技术文章
分享
下一篇:
这是最后一篇
上一篇:
这是第一篇
写评论...
发表评论
登录评论
匿名评论
提交
提交