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:

  1. Definition of services
interface ApiService {
    @GET("users")
    suspend fun getUsers(a): List<User>

}
Copy the code
  1. 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
  1. 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
  1. 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:

  1. The processing of exceptions is too granular. If you need to handle different exceptions differently, it can be troublesome.
  2. A try is needed at each point of the call… Catch the operation.
  3. 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:

  1. 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.
  2. 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:

  1. Normal communication with the server
  2. Get the header data in the response body
  3. Get error message from server (HTTP code, message)
  4. 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
  1. 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
  1. Returns the definition of the dataApiResult.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.

  1. Definition of servicesWanAndroidApi
interface WanAndroidApi {
    @GET("/banner/json")
    suspend fun getBanner(a): ApiResult<ApiResponse<List<Banner>>>
}
Copy the code
  1. To define aApiCallAdapterFactory.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
  1. Specified when Retrofit is initializedCallAdapterFactory, define fileApiServiceCreator.ktAs 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
  1. 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.

  1. Definition of servicesWanAndroidApi
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.

  1. To define aCallWait.ktFile forCallClass adds extension methodsawaitResultThe method has some internal logic and the aboveCallAdapterThe implementation is similar in.CallWait.ktThe 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
  1. 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
  1. inViewModelIs basically the same as method one, but it needs to be called hereawaitResultmethods
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
  1. If we want to retrieve data from reponse’s headers, we can use Retrofit’s extension functionsawaitResponseThat 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
  • ApiExceptionTo 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
  • NetResultEncapsulate 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
  1. 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

  1. 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.
  2. 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.
  3. 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