yunshuipiao / Potato

Read the fucking source code for the Android interview
Apache License 2.0
80 stars 12 forks source link

Android:How to make network requests gracefully #72

Open yunshuipiao opened 4 years ago

yunshuipiao commented 4 years ago

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 扩展程序中。请务必在使用这些范围时添加相应的依赖项。

改动如下: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

总结

  1. 在 Retrofit 中使用协程更好的进行网络请求
  2. 使用带状态的 ViewModel 的更好的进行 UI 交互。