preface

Retrofit is a very popular web request framework in the industry right now. It’s so easy to use that it’s practically a must for Android development. It also uses a lot of design patterns, its code is worth reading, its design ideas are worth thinking about…

To prevent the big guys from trolling me, let’s take a look at Retrofit, which is easier to use than Retrofit in a Kotlin+ coroutine environment.

First, two questions:

  1. Is it necessary to wrap network requests?

    Personally, I think it is necessary to take Retrofit for example, the intrusion of business code is relatively large. From a long-term perspective, no framework dare to say that it is YYDS. Once a new technology comes out and you want to change the framework, there will be 10,000 alpacas gallop through in your mind when facing so much business code.

  2. Is Retrofit still necessary if it’s a packaging layer?

    Why use Retrofit in the first place? It does three main things compared to OkHttp: 1. Thread switching. 2. Data analysis. 3. Request parameters can be configured.

    Thread switching is easy to implement with Kt coroutines. For data parsing, if you can get the Type that returns the result, that’s a line of code. Request parameters can also be easily set through simple encapsulation.

So the purpose of this article is to get rid of Retrofit and wrap OkHttp with Kt coroutines.

Take a look at the final use:

viewModelScope.launch(Dispatchers.IO) { 
    // Thanks for playing android API
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // Request the network to return data
    val result = HttpUtils.get<List<Chapters>>(url)
}
Copy the code

A clear purpose

All we want with a web request tool is that we pass in the parameters and return the expected results.

At this point in mind should probably have the desired effect:

HttpUtils.get(url, param, header, object: Callback<T>{ 
    onSuccess{}
    onError{}
})

HttpUtils.post(url, body, header, object: Callback<T>{ 
    onSuccess{}
    onError{}
})
Copy the code

The parameters to be passed include URL, Query, Body, Header, and so on. In order to return the parsed result, you need to pass the Type of the result Type. How to pass the Type that returns the result? The next two projects revolve around this problem.

Plan a

First define the return result:

data class ChaptersResp() {
    var data = arrayListOf<Chapters>(),
    var errorCode: Int.var errorMsg: String
}

data class Chapters(
    var courseId: String,
    var id: Int.var name: String,
    var order: Int
)
Copy the code

As mentioned earlier, the focus is on how to pass the Type of the expected return Type.

If it is an object such as ChaptersResp, we can pass it as chaptersresp.class, but projects typically have a BaseResp, so you only need to define a Chapters. How do you pass List

? Can’t just chapter.class with a List

.

1, Object. Class mode

If you must pass the Object. Class method, there are two ways: 1. Pass the class of the whole Object, such as chaptersresp.class. Httputils.get (); httputils.getList (); httputils.getList (); As follows:

// 1, pass the whole object
HttpUtils.get(url, param, ChaptersResp.class.object: HttpCallback<ChaptersResp> {
    onSuccess{}
    onError{}
})

// 2, methodically resolved: Pass Chapters' class and parse to List in the getList method
HttpUtils.getList(url, param, Chapters.class.object: HttpCallback<List<Chapters>> {
    onSuccess{}
    onError{}
})
Copy the code

This should be the lowest way, only beginners will do it, so generally do not do it.

You can see from the above method that the expected return type has been added through generics in the callback. Can we get from that?

2. Get from generics

In generic classes, getGenericSuperclass() gets the Type of the immediate parent of the entity (class, interface, primitive Type, or void) represented by the current class, and getActualTypeArguments() gets the array of arguments. As follows:

public abstract class HttpCallback<T> implements CallBack<T> {

    @Override
    public void onNext(String data) {
        / / get GenericSuperclass
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            // Get the Type array of the generic Type
            Type[] types = ((ParameterizedType) type).getActualTypeArguments();
            // Since there is a generic T, the first one is the Type of the generic passed in
            T result = new Gson().fromJson(data, types[0]);
            onSuccess(result, "" + data.getCode(), data.getMsg());
        } else {
            throw newClassCastException(); }}}Copy the code

After receiving the result of the request, the callback’s onNext() returns the result to the callback for processing. After parsing the data in onNext(), call other callbacks such as onSuccess().

The request would then read:

HttpUtils.get(url, param, object: HttpCallback<List<Chapters>> {
    onSuccess{}
    onError{}
})
Copy the code

Summary: This scheme uses a callback method to call back the Type of the anonymous internal return Type, and finally call back the parsed result.

Scheme 2

Since we’re using Kt coroutines here, Kt coroutines allow us to implement asynchronous effects in a synchronous manner, which seems a little out of sync with callbacks, how do we return results directly like Retrofit does?

Without the callback object, the problem is still passing the type that returns the result.

Retrofit is a return Type configured on interface methods, and the Type of the method return Type can be retrieved in a dynamic proxy by calling method’s getGenericReturnType(), which in turn retrieves the Type of the desired return Type.

Call
      
       >
      
Type returnType = method.getGenericReturnType();
[List
      
       ]
      
Type[] types = returnType.getActualTypeArguments();
/ / get the List < Chapters >
Type returnType = types[0]
Copy the code

We can’t pre-type a method like Retrofit, so can we pass it through generic methods? Like:

val result = HttpUtils.get<List<Chapters>>(url, param, header)
Copy the code

With Java, the answer is no. Kotlin, however, can, based on two features Kotlin provides:

1. Inline functions

An inline function replaces the body of the called function directly where the function was called.

2. The generic reified keyword

A generic type marked with the reified keyword is implemented and is typically used with inline functions.

Start with the reified keyword.

1, understandreifiedThe keyword

When we use generics in Java, we can’t get a Class by generics. We usually pass the Class as a parameter, which is the same problem as scheme 1.

For example, when you start an activity, you can add extension functions to the activity:

fun <T : Activity> Activity.startActivity(clazz: Class<T>) {
    startActivity(Intent(this, clazz))
}
Copy the code

Call:

startActivity(Main2Activity::class.java)
Copy the code

Kotlin provides a keyword reified (Reification) that marks generics as instantiated type parameters to make something abstract more concrete or real. The generic Class can be obtained directly with inline.

Modify the extension function:

inline fun <reified T : Activity> Activity.startActivity(a) {
    startActivity(Intent(this, T::class.java))
}
Copy the code

Call:

startActivity<Main2Activity>()
Copy the code

Isn’t it very simple? A single line of code saves several letters.

2, the pre

So how can we use this feature of Kotlin in our packaging layer?

Start with a simple test:

/ / define
inline fun <reified T> request(url: String) {
    val clazz = T::class.java
    LogUtil.e(clazz.toString())
}

/ / call
request<List<String>>("www.baidu.com")
Copy the code

Print the following:

–>interface java.util.List

Find that the List is retrieved, but the String is erased, so you can’t get the full Type for nested generics. What the ** ! What should I do?

Recall how Gson parses nested generics such as List

in Gson’s comments:

// Get the Type of List
      
        through an empty anonymous inner class
      
Type listType = new TypeToken<List<String>>() {}.getType();

List<String> target = new LinkedList<String>();
target.add("blah");
Gson gson = new Gson();
// Object to json
String json = gson.toJson(target, listType);
/ / json parsing
List<String> target2 = gson.fromJson(json, listType);
Copy the code

It fetches the Type of List

through an empty anonymous inner class. Looking at the TypeToken code, it fetches the Type of List

in the same way as scenario 1, except that it wraps a class to handle this problem:

class TypeToken<T>{
    
    final Type type;

    protected TypeToken(a) {
        this.type = getSuperclassTypeParameter(getClass());
    }

    static Type getSuperclassTypeParameter(Class
        subclass) { Type superclass = subclass.getGenericSuperclass(); . ParameterizedType parameterized = (ParameterizedType) superclass;return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); }... }Copy the code

Can we get the Type of the generic T in the same way? Try it on:

inline fun <reified T> request(url: String) {
    val type = object : TypeToken<T>() {}.type
    LogUtil.e(type.toString())
}
Copy the code

Print result:

–>java.util.List<? extends java.lang.String>

If you find that you can get its full Type, you can pass it as a parameter.

3, the actual combat

/ / get request
suspend inline fun <reified T> get(
    url: String,
    param: HashMap<String, Any>? = null,
    headers: HashMap<String, String>? = null
): T {
    val returnType = object : TypeToken<T>() {}.type
    return get(url, param, headers, returnType)
}

// Wrap the request parameters of the GET request, and finally make the unified request through execRequest()
suspend fun <T> get(
    url: String,
    param: HashMap<String, Any>? = null,
    headers: HashMap<String, String>? = null,
    returnType: Type
): T {
    valurlBuilder = HttpUrl.parse(url)!! .newBuilder() param? .let { it.keys.forEach { key -> urlBuilder.addQueryParameter(key, it[key].toString()) } }return execRequest(
        "GET",
        urlBuilder.build(),
        headers,
        nullReturnType)},// Todo post, PUT,delete request, etc

// unified request method
suspend fun <T> execRequest(
    method: String,
    httpUrl: HttpUrl,
    headers: HashMap<String, String>? = null,
    requestBody: RequestBody? , returnType:Type
): T {
    valrequest = Request.Builder().url(httpUrl).method(method, requestBody) headers? .keys? .forEach { request.addHeader(it, headers[it]) }try {
        OkHttpUtils.mClient.newCall(request).execute().use { response ->
            valbody = response.body()? .string()val jsonObject = JSONObject(body)
            val code = jsonObject.get("errorCode")
            when (code) {
                0- > {val data = jsonObject.get("data").toString()
                    return Gson().fromJson(data, returnType)
                }
				...
                else- > {throw MyException("Business exception:? code")}}}}catch (e: Throwable) {
        throw e
    }
}
Copy the code

OkHttp is a simple wrapper that makes it easy to initiate a network request:

viewModelScope.launch(Dispatchers.IO) { 
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // Request the network to return data
    val result = HttpUtils.get<List<Chapters>>(url)
}
Copy the code

Summary: This solution does not use callbacks, but uses the generic method to get the return Type Type with the help of Kotlin’s features, and returns the parsed result directly.

Unified handling of status and exceptions

The following is not the focus of this article, just to discuss how to handle the status and exception in the request, if there is a deficiency or a better solution please feel free to comment.

Because Kt coroutines are used, network requests run in coroutine IO, so OkHttp synchronous requests are used, which requires try-catch for network requests.

In the ViewModel+LiveData scenario, if there are multiple network requests, there is a problem: you need to define multiple Start/Finish and Error LiveData states for the UI layer to listen on. In general, these states may perform the same operations: Loading is started when the state is started, Loading is stopped when the state is finished, and an exception message is displayed when the state is abnormal. Therefore, these states need to be encapsulated.

There are also many related encapsulation schemes. Here is a simple scheme for reference only.

Since the Start/Finish/Error states are usually handled uniformly, they should be sealed in a sealed class.

sealed class LoadState {
    /** * start */
    class Start(var tip: String = "Loading...") : LoadState()

    /** * exception */
    class Error(val msg: String) : LoadState() /** * end */object Finish : LoadState
}
Copy the code

LoadState LiveData is defined in BaseViewModel for the View layer to listen on:

Open class BaseViewModel() : ViewModel() {// loadState = MutableLiveData< loadState >()... }Copy the code

In the UI:

The viewModel. LoadState. Observe (this) {the when (it) {is loadState. Start - > {/ / todo begin loading} is loadState. Error - > {/ / todo } is LoadState.Finish -> {// Todo load complete}}}Copy the code

With the observer and the observed, when do you distribute the data?

Again, in BaseViewModel, the network request is executed in the coroutine as a higher-order function, as follows:

open class BaseViewModel() : ViewModel() {

    // Load status
    val loadState = MutableLiveData<LoadState>()
    
	// Use this method to initiate a network request
    private fun launch(block: suspend CoroutineScope. () - >Unit) {
        viewModelScope.launch() {
            try {
                withContext(Dispatchers.IO) {
                    // Execute the network request code block
                    block.invoke(this)}}catch (e: Throwable) {
                // handle error
                val error = ExceptionUtils.parseException(e)
                loadState.value = LoadState.Error(error)
            } finally {
                loadState.value = LoadState.Finish
            }
        }
    }
}
Copy the code

In the ViewModel:

val chapters = MutableLiveData<List<Chapters>>()

fun request(a){
    launch {
        val url = "https://wanandroid.com/wxarticle/chapters/json"
    	// Request the network to return data
    	val result = HttpUtils.get<List<Chapters>>(url)
        chapters.postValue(result)
    }
}
Copy the code

This allows the ViewModel to simply launch the network request in the IO thread and automatically distribute the Start/Finish/Error status.

In the UI, you only need to listen for LoadState and LiveData, the result of the network request.

At this point, the simple network request encapsulation in Kotlin+ViewModel+LiveData environment is complete, but there are still some problems:

Question 1:

Some requests are executed silently in the background and do not need to handle the state of the start-end exception.

This can be resolved from the BaseViewModel by adding the launchSlient function, like the launch function, which controls whether to distribute LoadState.

Question 2:

Loading must be displayed during multiple requests at the same time, but when one of them is completed first, the callback is performedLoadState.FinishOther requests are still in progress but Loading has been disabled.

AtomicInteger can be operated atomically in BaseViewModel to record the number in the current request, and loadstate.finish can be called when the number is zero.

Question 3:

Some service exceptions may require special handling and cannot be handled in a unified manner.

In this case, it needs to be handled at the packaging layer. Unified exception handling can be used as a bottom-of-the-pocket strategy. For special service exceptions, they are not thrown out after being captured, but handled by higher-order functions:

launch {
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // Request the network to return data
    val result = HttpUtils.get<List<Chapters>>(url){ error->
		// Todo handles exceptions}}Copy the code

conclusion

This article focuses on a solution to simplify network requests by wrapping OkHttp in kotlin+ coroutine + ViewModel + LiveData.

This is not to say that Retrofit can be completely abandoned, but Retrofit is a large, comprehensive package of web requests that can meet all kinds of needs.

In this paper, only dozens of lines of code to achieve the GET request, detailed code and other packages can be referred to my open source project Cloud Weather (github.com/wdsqjq/Feng…)

The above solutions are suitable for simple network request scenarios, and need to be extended for special requirements.