背景
想在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
override fun toString(): String { return userId + userName + describe } }
|
定义请求路径和参数等
这里getUserInfo的返回值类型是retrofit2.Call,
但也只是一种请求用的方法,
事实上还有Response类型的
1 2 3 4 5 6 7 8 9
| interface NetworkApi {
@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
|
val retrofit = Retrofit.Builder() .baseUrl("http://192.168.1.2:8080/") .addConverterFactory(GsonConverterFactory.create()) .build()
val networkApi = retrofit.create(NetworkApi::class.java)
networkApi.getUserInfo("3").enqueue(object : Callback<UserInfo?> { override fun onResponse(call: Call<UserInfo?>, response: Response<UserInfo?>) { Log.d("response", response.body().toString()) }
override fun onFailure(call: Call<UserInfo?>, t: Throwable) { Log.d("response", "connection failed") } })
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 { private fun retrofit(): Retrofit = Retrofit.Builder() .baseUrl("http://192.168.1.2:8080/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .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() } )
|
更高级的数据封装
其实不明白为什么要封装,
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结果.