retrofit使用

背景

想在viewModel中使用进行网络请求,由于使用的是volley,
volley自行使用context来监视生命周期来自动取消防止内存泄露.
因此碰到了在viewModel中引用view层的大忌,
初次之外db的查询使用的是anko-sql,也遇到了这样的问题,
而听说网络请求方面最常用的是retrofit,

  • 因为基于okhttp性能好
  • 因为兼容大数据包
  • 因为解耦彻底

并且也不需要context,因此决定从volley转向retrofit.

介绍

是Square公司的开源产品,
底层基于OkHttp,
面向RESTful风格API设计.
目前最新版本是2.7.2
流程:
app应用层->Retrofit->OkHttp->服务器
服务器->OkHttp->Retrofit->app应用层

基础使用

依赖

通常除了retrofit以外还会搭配一些解析返回数据的工具,
比如gson

1
2
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

返回数据的类

该类和返回数据的结构息息相关,
基础使用中先使用一层.

如果服务器返回值类型不是一层,
而是有许多层时,建议用两个bean包装一下.

1
2
3
4
5
6
7
8
9
10
class UserInfo {
private val userId: String? = null
private val userName: String? = null
private val describe: String? = null

// for TEST
override fun toString(): String {
return userId + userName + describe
}
}

定义请求路径和参数等

这里getUserInfo的返回值类型是retrofit2.Call,
但也只是一种请求用的方法,
事实上还有Response类型的

1
2
3
4
5
6
7
8
9
interface NetworkApi {
/**
* 获取用户信息
* @return
* @Query 注解
*/
@GET("getUserInfo")
fun getUserInfo(@Query("id") userId: String?): Call<UserInfo?>
}

开始使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 定义retrofit实例
// 根路由带斜线则具体的请求路径时不需要带
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.1.2:8080/")
.addConverterFactory(GsonConverterFactory.create())
.build()

// 看起来是一个接口有了实体化的对象
val networkApi = retrofit.create(NetworkApi::class.java)

// Call类型才有的enqueue方法
// 这个是异步请求
networkApi.getUserInfo("3").enqueue(object : Callback<UserInfo?> {
override fun onResponse(call: Call<UserInfo?>, response: Response<UserInfo?>) {
// body()会被自动转换类型到UserInfo,神奇
Log.d("response", response.body().toString())
}

override fun onFailure(call: Call<UserInfo?>, t: Throwable) {
Log.d("response", "connection failed")
}
})

// 在主线程(UI线程)中使用同步请求,谷歌以前不推荐,现在不允许
val response: Response<UserInfo?> = networkApi.getUserInfo("3").execute()
if (response.isSuccessful) {
Log.d("response2", response.body().toString())
} else {
Log.d("response3", response.code().toString() + response.message())
}

// 通常使用协程提供一个异步的环境来执行同步请求代码
GlobalScope.launch {
val response: Response<UserInfo?> = networkApi.getUserInfo("3").execute()
}

注意

在OkHttp 3.13版本后,其要求使用java8,在语言使用kotlin时需要在项目定义中写

1
2
3
4
5
6
android {
compileOptions {
targetCompatibility = "8"
sourceCompatibility = "8"
}
}

初步封装

想在协程中写同步请求来达到异步效果

不写callback往往可以省略众多的override函数,
代码量减少的同时也更好理解.

最简单的协程可以使用 GlabalScope

1
2
3
4
5
6
7
8
GlobalScope.launch {
val response: Response<UserInfo?> = networkApi.getUserInfo("3").execute()
if (response.isSuccessful) {
Log.d("response2", response.body().toString())
} else {
Log.d("response3", response.code().toString() + response.message())
}
}

在viwemodel中则可以使用专用的 viewModelScope

不想在每个地方都实例化retrofit

无论是同步请求,还是异步请求,
目前所知都是使用一个对象的方法,
来获取结果,因此可以传递该对象到其他文件来处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object ApiService {
//构建retrofit
private fun retrofit(): Retrofit = Retrofit.Builder()
.baseUrl("http://192.168.1.2:8080/")
.addConverterFactory(GsonConverterFactory.create())//gson解析json适配器,自动将json解析为对象
.addCallAdapterFactory(CoroutineCallAdapterFactory())//支持kotlin协程
.build()

//创建接口对象
val api: NetworkApi = retrofit().create(NetworkApi::class.java)

// 执行请求动作并在做基础的错误处理后返回值
fun <T : Any?> execute(call: Call<T>): T? {
val response = call.execute()
var result: T? = null
if (response.isSuccessful) {
result = response.body()
} else {
Log.d("error", response.code().toString() + response.message())
result = null
}
return result
}
}

这样在发起请求的文件中就可以使用

1
2
3
4
5
6
7
8
GlobalScope.launch {
val res = ApiService.execute(
call = ApiService.api.getUserInfo("3")
)
if (res != null) {
Log.d("response", res.toString())
}
}

只需要将网络请求过程看成一个有可能抛出null值的本地api即可

更进一步地,还可以不传递Call的对象而是传递一个函数定义.
稍后再看一起使用的效果.

别人用的封装

这些封装不知道意义在什么地方,
可能需要大量使用才能有心得

使用协程相关集成

就是把请求API的返回类型改成 Response,
然后也不在传递 Call 对象而是传递函数.

首先改变请求API的返回值类型,
这个 Deserred 是协程需要的.

1
2
@GET("getUserInfo")
fun getUserInfo(@Query("id") userId: String): Deferred<Response<UserInfo?>>

触发的方式也改了.
call参数被改成了函数,
suspend是kotlin协程的核心,特色是异步返回,
其启动的子协程会让父协程挂起并等待返回,
以此达到同步调用的效果.

1
2
3
4
5
6
7
8
9
10
suspend fun <T : Any?> execute(call: suspend () -> Response<T>): T? {
val response = call.invoke()
var result: T? = null
if (response.isSuccessful) {
result = response.body()
} else {
Log.d("error", response.code().toString() + response.message())
}
return result
}

调用的方式也有些变化
call的赋值使用的是大括号,kotlin中常用的添加扩展或传递函数的方法.

1
2
3
4
val res = ApiService.execute(
// call = ApiService.api.getUserInfo("3").await()
call = { ApiService.api.getUserInfo("3").await() }
)

更高级的数据封装

其实不明白为什么要封装,
response.body(),
response.code().toString(),
response.message() 明明用着很方便.

明明可以
Response<UserInfo> --> UserInfo
非要去使用
Response<UserInfo> --> DataResult<UserInfo> --> UserInfo

封装用模型引入

1
2
3
4
5
6
sealed class DataResult<out T : Any> {
//成功
data class Success<out T : Any>(val result: T?) : DataResult<T>()
//失败
data class Error(val errorCode:String?,val errorMessage: String?) : DataResult<Nothing>()
}

内部处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
suspend fun <T : Any> execute(call: suspend () -> Response<T>): T? {
val data = parseApiResult(call)
var result: T? = null
when (data) {
is DataResult.Success -> result = data.result
is DataResult.Error -> {
//请求失败
Log.d("error",data.errorMessage)
}
}
return result
}

private suspend fun <T : Any> parseApiResult(call: suspend () -> Response<T>): DataResult<T> {
try {
val response = call.invoke()
if (response.isSuccessful) {
return DataResult.Success(response.body())
}
return DataResult.Error(response.code().toString(), response.message())
}catch (e:Exception){
Log.d("error", e.message)
return DataResult.Error("", e.message)
}
}

注意这样需要稍微改动请求API的返回值类型,
从有 ? 到没有 ?
而改确定之后程序仍然可以返回空,
这个效果是execute函数内部使用判断,并返回 T? 来实现的.

1
fun getUserInfo(@Query("id") userId: String): Deferred<Response<UserInfo>>

或许这种封装统一了模型出错时的反应,无论是服务器错误还是客户端错误,
都渲染出null结果.