Recently, I have been investigating the practice of using Kotlin coroutine + Retrofit to make network request scheme, which will be introduced into the new project later. The use of Retrofit is very simple, and basically you can access it immediately after reading a document. I also found a lot of demos on Github to see how others write and read a lot of online articles. However, I found that many articles were just a simple access Demo, which could not meet my current business needs. The results of recent research and our use are recorded below. First of all, let’s compare several schemes found on the Internet:
Plan a
This is a very good introduction to Kotlin coroutines + Retrofit, and the code is as follows:
- Definition of services
interface ApiService {
@GET("users")
suspend fun getUsers(a): List<User>
}
Copy the code
- Retrofit Builder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(a): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Copy the code
- Some intermediate layers
class ApiHelper(private val apiService: ApiService) {
suspend fun getUsers(a) = apiService.getUsers()
}
Copy the code
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers(a) = apiHelper.getUsers()
}
Copy the code
- Get network data in the ViewModel
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
fun getUsers(a) = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ? :"Error Occurred!"))}}}Copy the code
This code communicates with the server, meets basic requirements, and also has a mechanism for handling exceptions. But there are the following problems:
- The processing of exceptions is too granular. If you need to handle different exceptions differently, it can be troublesome.
- A try is needed at each point of the call… Catch the operation.
- It is not supported to obtain the response header, HTTP code information from reponse. However, many apps don’t require this processing either. If you don’t get the data, just give a general prompt. So this scheme can be used directly in some cases.
Scheme 2
BaseRepository tries calls to interfaces that are called from the repository. Catch handlers, so that callers don’t have to add a try to each… The catch. The relevant code is as follows:
open class BaseRepository {
suspend fun <T : Any> apiCall(call: suspend() - >WanResponse<T>): WanResponse<T> {
return call.invoke()
}
suspend fun <T : Any> safeApiCall(call: suspend() - >Result<T>, errorMessage: String): Result<T> {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}
suspend fun <T : Any> executeResponse(response: WanResponse<T>, successBlock: (suspend CoroutineScope. () - >Unit)? = null,
errorBlock: (suspend CoroutineScope. () - >Unit)? = null): Result<T> {
return coroutineScope {
if (response.errorCode == -1) { errorBlock? .let { it() } Result.Error(IOException(response.errorMsg)) }else{ successBlock? .let { it() } Result.Success(response.data)}}}}Copy the code
Write this in Repository
class HomeRepository : BaseRepository() {
suspend fun getBanners(a): Result<List<Banner>> {
return safeApiCall(call = {requestBanners()},errorMessage = "")}private suspend fun requestBanners(a): Result<List<Banner>> =
executeResponse(WanRetrofitClient.service.getBanner())
}
Copy the code
Plan 3
Reading this blog on the Internet, the author uses a CallAdapter to convert HTTP errors into exceptions and throw them out (my own plan 1 follows this idea later). The core code is as follows:
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(a): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
/** * This method is called by Retrofit code that handles the suspend method and passes in a callback. If you call callback.onResponse, The suspend method returns successfully. If you call callback.onFailure, the suspend method throws an exception So our implementation here is always to call callback.onResponse, only to return apiResult. Success on Success and apiResult.Failure on Failure. Apiresult. Success or apiresult. Failure */ is returned
override fun enqueue(callback: Callback<ApiResult<T> >) {
// Delegate is the Call
object used to make the actual network request, and the success or failure of the network request will Call back to different methods
delegate.enqueue(object : Callback<T> {
/** * This method is called back when the network request returns successfully (whether the status code is 200 or not) */
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {/ / HTTP status is 200 +
// Response.body () might be null(we haven't detected this yet)
// An additional benefit of this handling is that we can ensure that the object we pass to apiResult. Success is not null, so we don't have to declare null when we use it outside
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {/ / HTTP status error
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
}
If the network request succeeds, but the business fails, we will throw an exception in the corresponding Interceptor, which will also call this method */
override fun onFailure(call: Call<T>, t: Throwable) {
val failureApiResult = if (t is ApiException) {The Interceptor throws ApiException to end the request directly and the ApiException contains an error message
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}
...
}
Copy the code
The author has provided a Demo, if you want to use it, you need to add a wrapper class that returns data. The downside of this scenario is that you can’t get the header in the response body, but again, this requirement is so uncommon that you can ignore it.
To summarize, there may be limitations to some of the solutions currently available online:
- If the server has an error, you cannot get a specific error message. For example, if the server returns 401, 403, the network layer in these scenarios cannot pass this information out.
- If the server passes data to the front end through the header, these scenarios are not sufficient.
In view of the above two problems, let’s consider how to improve the implementation of the framework.
Adjust to the idea
We expect a network request scheme to meet the following objectives:
- Normal communication with the server
- Get the header data in the response body
- Get error message from server (HTTP code, message)
- Convenient exception handling
The adjusted scheme
The relevant dependency library versions of the following code
implementation 'com. Squareup. Retrofit2: retrofit: 2.8.1'
implementation "Com. Squareup. Retrofit2: converter - gson: 2.8.1"
//Coroutine
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.6." "
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.3.6." "
Copy the code
- Agree common error types
We expect the ApiException to return an HTTP Code as well. For this convention, the error message Code starts at 20000 so that it does not conflict with the HTTP Code.
ApiError
object ApiError {
var unknownError = Error(20000."unKnown error")
var netError = Error(20001."net error")
var emptyData = Error(20002."empty data")}data class Error(var errorCode: Int.var errorMsg: String)
Copy the code
- Returns the definition of the data
ApiResult.kt
It is used to carry the returned data, return normal business data on success, and assemble errorCode, errorMsg on error, which will be thrown up to the caller.
sealed class ApiResult<out T>() {
data class Success<out T>(val data: T):ApiResult<T>()
data class Failure(val errorCode:Int.val errorMsg:String):ApiResult<Nothing>()
}
data class ApiResponse<out T>(var errorCode: Int.var errorMsg: String, val data: T)
Copy the code
Plan a
This scheme supports fetching HTTP Code and returning it to the caller, but does not support extracting header data from HTTP Response.
- Definition of services
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(a): ApiResult<ApiResponse<List<Banner>>>
}
Copy the code
- To define a
ApiCallAdapterFactory.kt
In this, the response data is filtered and, in case of an error, an error is thrown out.
class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {=
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) { "$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }
val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter<Any>(dataType)
}
}
class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(a): Type = type
override fun adapt(call: Call<T>): Call<ApiResult<T>> {
return ApiResultCall(call)
}
}
class ApiResultCall<T>(private val delegate: Call<T>) : Call<ApiResult<T>> {
override fun enqueue(callback: Callback<ApiResult<T> >) {
delegate.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
The Interceptor throws ApiException to end the request directly and the ApiException contains an error message
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}
override fun clone(a): Call<ApiResult<T>> = ApiResultCall(delegate.clone())
override fun execute(a): Response<ApiResult<T>> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")}override fun isExecuted(a): Boolean {
return delegate.isExecuted
}
override fun cancel(a) {
delegate.cancel()
}
override fun isCanceled(a): Boolean {
return delegate.isCanceled
}
override fun request(a): Request {
return delegate.request()
}
override fun timeout(a): Timeout {
return delegate.timeout()
}
}
Copy the code
- Specified when Retrofit is initialized
CallAdapterFactory
, define fileApiServiceCreator.kt
As follows:
object ApiServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()
private fun getRetrofit(a) = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()
fun <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(a): T = create(T::class.java)
}
Copy the code
- Use the following in ViewModel:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
Log.i("API Response"."--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response"."errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")}}}Copy the code
Scheme 2
Based on scheme 1, this scheme supports data retrieval from HTTP Response Header.
- Definition of services
WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call<ApiResponse<List<Banner>>>
}
Copy the code
It is important to note that the getBanner2() method here does not start with the suspend keyword and returns an object of type Call.
- To define a
CallWait.kt
File forCall
Class adds extension methodsawaitResult
The method has some internal logic and the aboveCallAdapter
The implementation is similar in.CallWait.kt
The document also draws on this passagecode
suspend fun <T : Any> Call<T>.awaitResult(a): ApiResult<T> {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>? , response:Response<T>) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data= =null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!) }}else {
ApiResult.Failure(response.code(), response.message())
}
})
}
override fun onFailure(call: Call<T>, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
Copy the code
- Initialization of Retrofit
Unlike scheme 1, there is no need to specify the CallAdapterFactory during Retrofit initialization. The apiservicecreator.kt file is defined
object ApiServiceCreator {
private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()
private fun getRetrofit(a) = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass)
inline fun <reified T> create(a): T = create(T::class.java)
}
Copy the code
- in
ViewModel
Is basically the same as method one, but it needs to be called hereawaitResult
methods
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse<List<Banner>>
Log.i("API Response"."--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response"."errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")}}}Copy the code
- If we want to retrieve data from reponse’s headers, we can use Retrofit’s extension functions
awaitResponse
That is as follows:
try {
val result = api.getBanner2().awaitResponse()
// Take the data from the HTTP Header
Log.i("API Response"."-----header---->Server:" + result.headers().get("Server"))
if (result.isSuccessful) {
var data = result.body();
if (data! =null && data is ApiResponse<List<Banner>>) {
Log.i("API Response"."--------->data:" + data.data.size)
}
} else {
/ / HTTP Code
Log.i("API Response"."errorCode: ${result.code()}")}}catch (e: Exception) {
Log.i("API Response"."exception: ${e.message}");
}
Copy the code
Plan 3
If we implement a set in Java
- Define the service
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call<NetResult<List<Banner>>> getBanner();
}
Copy the code
ApiException
To encapsulate the error message
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;
public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}
public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}
public String getErrorMessage(a) {
return this.errorMessage;
}
public int getErrorCode(a) {
return this.errorCode;
}
interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}
public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, "Data parsing error");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, "Server response error");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, "Network connection error");
}
Copy the code
NetResult
Encapsulate the server’s response
public class NetResult<T> {
private T data;
private int code;
privateString errorMsg; .Get/set / / ignore
}
Copy the code
- Define a Callback to parse the data
public abstract class RetrofitCallbackEx<T> implements Callback<NetResult<T>> {
@Override
public void onResponse(Call<NetResult<T>> call, Response<NetResult<T>> response) {
// If success is returned
if (response.isSuccessful()) {
NetResult<T> data = response.body();
Code == 0 in the returned data indicates that the service is successful
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
// Data parsing erroronFail(ApiException.PARSE_ERROR); }}else{ onFail(ApiException.SERVER_ERROR); }}else {
// Server request error
Log.i("API Response"."code:" + response.code() + " message:"+ response.message()); onFail(ApiException.SERVER_ERROR); }}@Override
public void onFailure(Call<NetResult<T>> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}
protected abstract void onSuccess(T t);
protected abstract void onFail(ApiException e);
}
Copy the code
- use
api.getBanner().enqueue(new RetrofitCallbackEx<List<Banner>>() {
@Override
protected void onSuccess(List<Banner> banners) {
if(banners ! =null) {
Log.i("API Response"."data size:"+ banners.size()); }}@Override
protected void onFail(ApiException e) {
Log.i("API Response"."exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: "+ e.getMessage()); }});Copy the code
other
- In a real world project, you will often need to handle HTTP Code globally, such as directing the user to the login page when the server returns 401. This global interception should be placed directly in the interceptor.
- The scheme of architecture is to meet the needs of the business, and here I just comb and investigate the business scenarios I encounter. Of course, there are often more requirements in actual projects, such as different domain names caused by environment switching, general configuration of network requests, reporting of service anomalies, etc., a complete network request scheme needs to add more functions.
- The Kotlin language is very flexible, and the use of extension functions makes the code very concise. Kotlin has not used much in our project and is not very proficient. Coroutine + Retrofit should have a more elegant way to write it. Welcome to exchange.
reference
-
Blog.mindorks.com/using-retro…
-
Github.com/lulululbj/w…
-
Github.com/gildor/kotl…
-
How does Retrofit support suspend