Potato
Potato copied to clipboard
Android:How to make network requests gracefully
Android:How to make network requests gracefully
[TOC]
Retrofit2
在 retrofit2 的 2.6.0 版本中,增加了对 kotlin coroutines 的支持。
前提
在一般的业务中,请求服务器返回的结果都有如下的格式:
{
"result": 0,
"message": "",
"data": "swensun"
}
{
"result": 101,
"message": "parameter error"
}
第一种情况表示请求成功,服务器根据业务返回响应的结果。这里暗含的前提是服务器可以处理请求,不包括网络错误等异常情况(后续处理)。
第二种情况表示请求失败,服务器给出对应的错误码进行后续处理。
因此可以定义以下类型:表示服务器的响应结果。
class BaseResponse<T> {
@SerializedName("result")
var result = 0
@SerializedName("message")
var message = ""
@SerializedName("data")
var data: T? = null
val success: Boolean
get() = result == 0
}
网络请求
下面处理 okhttp 的 retrofit2:
class HttpClient {
//模拟请求,通过拦截器模拟请求数据
var base_url = "https://apitest.com" //
val okHttpClient by lazy {
OkHttpClient.Builder()
.build()
}
val retrofit by lazy {
Retrofit.Builder().client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(base_url).build()
}
}
下面开始进行模拟的网络请求:
interface ApiService {
@GET("/data/")
suspend fun fetchData(): BaseResponse<String>
}
注意这里的区别,函数前加上 suspend 关键字,返回值为服务器返回的数据即可。
下面利用 okhttp 的拦截器去模拟服务器的响应数据。
class MockResponseIntercepotr : Interceptor {
/**
* 目前没有真实服务器,利用拦截器模拟数据返回。
* 分别返回一次失败,一次成功
*/
var count = 0
override fun intercept(chain: Interceptor.Chain): Response {
var result: BaseResponse<String>
if (count % 2 == 0) {
result = BaseResponse<String>().apply {
this.result = 0
this.data = "swensun"
}
} else {
result = BaseResponse<String>().apply {
this.result = 101
this.message = "server error"
}
}
count += 1
return Response.Builder()
.body(ResponseBody.create(null, GsonUtils.toJson(result)))
.code(200)
.message(result.message)
.protocol(Protocol.HTTP_2)
.request(chain.request()).build()
}
}
下面利用retrofit2 的协程进行网络请求。viewModel 内容如下:
class CoroutinesViewModel : ViewModel() {
/**
* This is the job for all coroutines started by this ViewModel.
* Cancelling this job will cancel all coroutines started by this ViewModel.
*/
private val viewModelJob = SupervisorJob()
/**
* Cancel all coroutines when the ViewModel is cleared
*/
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
/**
* This is the main scope for all coroutines launched by ViewModel.
* Since we pass viewModelJob, you can cancel all coroutines
* launched by uiScope by calling viewModelJob.cancel()
*/
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val apiService = HttpClient.retrofit.create(ApiService::class.java)
fun fetchData() {
/**
* 启动一个协程
*/
uiScope.launch {
val timeDiff = measureTimeMillis {
val responseOne = apiFetchOne()
val responseTwo = apiFetchTwo()
Logger.d("responseOne:${GsonUtils.getGson().toJson(responseOne)}")
Logger.d("responseTwo:${GsonUtils.getGson().toJson(responseTwo)}")
}
Logger.d("timeDiff: $timeDiff")
}
}
private suspend fun apiFetchOne(): BaseResponse<String> {
/**
* 模拟网络请求,耗时 5s,打印请求线程
*/
Logger.d("apiFetchOne current thread: ${Thread.currentThread().name}")
delay(5000)
return apiService.fetchData()
}
private suspend fun apiFetchTwo(): BaseResponse<String> {
Logger.d("apiFetchTwo current thread: ${Thread.currentThread().name}")
delay(3000)
return apiService.fetchData()
}
}
UI 端负责网络请求,打印结果。
btn_fetch.setOnClickListener {
viewModel.fetchData()
}
2019-12-25 16:47:50.372 D/Logger: apiFetchOne current thread: main
2019-12-25 16:47:55.482 D/Logger: apiFetchTwo current thread: main
2019-12-25 16:47:58.542 D/Logger: responseOne:{"data":"swensun","message":"","result":0}
2019-12-25 16:47:58.545 D/Logger: responseTwo:{"message":"server error","result":101}
2019-12-25 16:47:58.546 D/Logger: timeDiff: 8187
结果如上,可以看到协程请求在 主线程执行,两个任务顺序执行,共计花费8s。 其中响应结果有成功和失败的情况。(MockResponseInterceptor)
其中 uiScope 开启协程,Dispatchers.Main + viewModelJob 前者指定执行线程, 后者可以取消协程操作。
并行处理
可以指定协程运行在 IO 线程,并通过 Livedata 通知 UI 线程更新UI。
如果两个请求无关联,可以通过 async 并行处理。
修改如下:
/**
* 启动一个协程
*/
uiScope.launch {
val timeDiff = measureTimeMillis {
withContext(Dispatchers.IO) {
val responseOne = async { apiFetchOne() }
val responseTwo = async { apiFetchTwo() }
Logger.d("responseOne:${GsonUtils.getGson().toJson(responseOne.await())}")
Logger.d("responseTwo:${GsonUtils.getGson().toJson(responseTwo.await())}")
}
}
Logger.d("timeDiff: $timeDiff")
}
2019-12-25 16:59:38.687 D/Logger: apiFetchOne current thread: DefaultDispatcher-worker-3
2019-12-25 16:59:38.689 D/Logger: apiFetchTwo current thread: DefaultDispatcher-worker-2
2019-12-25 16:59:43.751 D/Logger: responseOne:{"message":"server error","result":101}
2019-12-25 16:59:43.752 D/Logger: responseTwo:{"data":"swensun","message":"","result":0}
2019-12-25 16:59:43.756 D/Logger: timeDiff: 5086
可以看到,两个任务并行处理,花费时间为处理任务中耗时最多的任务。
异常处理
目前 MockResponseInterceptor 模拟响应都是服务器正确处理(code:200)的结果,但还会有其他异常,比如请求时网络异常,服务器内部错误,请求地址不存在等。
接着模拟一个服务器内部错误:
return Response.Builder()
.body(ResponseBody.create(null, GsonUtils.toJson(result)))
.code(500)
.message("server error")
.protocol(Protocol.HTTP_2)
.request(chain.request()).build()
此时继续请求,发生崩溃。协程执行中无法处理崩溃。
2019-12-25 17:05:54.444 29664-29664/com.swensun.potato E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.swensun.potato, PID: 29664
retrofit2.HttpException: HTTP 500 server error
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:129)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:206)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
处理一:
协程处理异常:
uiScope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
Logger.d("response error: ${throwable.message}")
}) {
val timeDiff = measureTimeMillis {
withContext(Dispatchers.IO) {
val responseOne = async { apiFetchOne() }
val responseTwo = async { apiFetchTwo() }
Logger.d("responseOne:${GsonUtils.getGson().toJson(responseOne.await())}")
Logger.d("responseTwo:${GsonUtils.getGson().toJson(responseTwo.await())}")
}
}
Logger.d("timeDiff: $timeDiff")
}
在启动协程出进行异常捕获,处理异常。
D/Logger: apiFetchTwo current thread: DefaultDispatcher-worker-3
D/Logger: apiFetchOne current thread: DefaultDispatcher-worker-2
D/Logger: response error: HTTP 500 server error
缺点在于每次启动协程都需要进行处理,并且代码处理方式不优雅。
处理二: 最佳实践
对于所有异常,不仅需要知道它是什么异常,并且还需要方便的进行处理。
利用 Okhttp 的拦截器:
class ErrorHandleInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
try {
val request = chain.request()
// 无网络异常
if (!NetworkUtils.isConnected()) {
throw NetworkErrorException("no network")
}
// 服务器处理异常
val res = chain.proceed(request)
if (!res.isSuccessful) {
throw RuntimeException("server: ${res.message()}")
}
return res
} catch (e: Exception) {
val httpResult = BaseResponse<String>().apply {
result = 900 // 901, 902
data = null
message = e.message ?: "client internal error"
}
val body = ResponseBody.create(null, GsonUtils.getGson().toJson(httpResult))
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.message(httpResult.message)
.body(body)
.code(200)
.build()
}
}
}
如上所示,定义异常处理,将上述异常转换为服务器正确响应的结果,自定义错误码,每个协议单独处理。
此时不用对协程异常进行捕获处理。
private val okHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(ErrorHandleInterceptor())
.addInterceptor(MockResponseInterceptor())
.build()
}
uiScope.launch {
val timeDiff = measureTimeMillis {
withContext(Dispatchers.IO) {
val responseOne = apiFetchOne()
val responseTwo = apiFetchTwo()
Logger.d("responseOne:${GsonUtils.getGson().toJson(responseOne)}")
Logger.d("responseTwo:${GsonUtils.getGson().toJson(responseTwo)}")
}
}
Logger.d("timeDiff: $timeDiff")
}
打印结果如下, 保证了代码的逻辑性。
D/Logger: apiFetchOne current thread: DefaultDispatcher-worker-1
D/Logger: apiFetchTwo current thread: DefaultDispatcher-worker-3
D/Logger: responseOne:{"message":"server: server error","result":900}
D/Logger: responseTwo:{"message":"server: server error","result":900}
D/Logger: timeDiff: 8096
补充: Important
在上述处理二的基础上,apiFetchOne 是同步执行, 执行过程中依然会抛出 IO 异常。
@Override public Response<T> execute() throws IOException {
okhttp3.Call call;
synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true;
...
}
同样,一种方式是对每一个请求进行 try catch。坏处在于破坏了函数执行顺序。还是按照处理二的方式,在抛出异常的情况下,依然返回相对应的 HttpResult,直接进行判断。
suspend fun <T : Any> safeApiCall(call: suspend () -> HttpResult<T>): HttpResult<T> {
return try {
call.invoke()
} catch (e: Exception) {
LogManager.d("retrofit error:${e.message}")
HttpResult<T>().apply {
result = HttpResultCode.INTERNAL_ERROR
data = null
message = e.message ?: "client internal error"
}
}
}
对 retrofit 接口进行安全判断。
val responseOne = safeApiCall{ apiFetchOne() }
val responseTwo = safeApiCall{ apiFetchTwo() }
Important, Important, Important
ViewModel 的交互
在每个网络请求中,UI 可能都对应不同的状态,比如加载中,加载成功,加载失败。
此时可使用 LiveData, 将状态通知到 UI ,UI 根据不同的页面显示不同的页面。
定义状态
enum class StateEvent {
LOADING,
SUCCESS,
ERROR
}
open class StateViewModel: ViewModel() {
val stateLiveData = MutableLiveData<StateEvent>()
protected fun postLoading(){
stateLiveData.postValue(StateEvent.LOADING)
}
protected fun postSuccess() {
stateLiveData.postValue(StateEvent.SUCCESS)
}
protected fun postError() {
stateLiveData.postValue(StateEvent.ERROR)
}
}
定义状态,定义viewModel 的扩展属性,用于观察数据,决定 UI 的显示。
viewModel.stateLiveData.observe(this, Observer {
when (it) {
StateEvent.LOADING -> {
Logger.d("loading")
}
StateEvent.SUCCESS -> {
Logger.d("SUCCESS")
}
StateEvent.ERROR -> {
Logger.d("ERROR")
}
}
})
在 UI 端就可以观察数据变动,改变 UI。
下面在网络请求过程中发出不同的状态:
postLoading()
uiScope.launch {
//loading
val timeDiff = measureTimeMillis {
withContext(Dispatchers.IO) {
val responseOne = apiFetchOne()
val responseTwo = apiFetchTwo()
if (responseOne.success && responseTwo.success) {
// success
postSuccess()
} else {
//error
postError()
}
}
}
Logger.d("timeDiff: $timeDiff")
}
打印结果如下:
D/Logger: loading
D/Logger: apiFetchOne current thread: DefaultDispatcher-worker-1
D/Logger: apiFetchTwo current thread: DefaultDispatcher-worker-2
D/Logger: ERROR
D/Logger: timeDiff: 8137
新版本 ViewModel
在下面的版本中,添加 KTX 依赖项
本主题中介绍的内置协程范围包含在每个相应架构组件的 KTX 扩展程序中。请务必在使用这些范围时添加相应的依赖项。
- 对于
ViewModelScope
,请使用androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01
或更高版本。 - 对于
LifecycleScope
,请使用androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01
或更高版本。 - 对于
liveData
,请使用androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01
或更高版本。
改动如下:viewModel 中使用,viewModelScope,所在的viewModel 被销毁时,自动取消所有协程的执行。
postLoading()
viewModelScope.launch {
//loading
val timeDiff = measureTimeMillis {
withContext(Dispatchers.IO) {
val responseOne = apiFetchOne()
val responseTwo = apiFetchTwo()
if (responseOne.success && responseTwo.success) {
// success
postSuccess()
} else {
//error
postError()
}
}
}
Logger.d("timeDiff: $timeDiff")
}
代码地址:https://github.com/yunshuipiao/Potato
总结
- 在 Retrofit 中使用协程更好的进行网络请求
- 使用带状态的 ViewModel 的更好的进行 UI 交互。