Kotlin has become Google’s first recommended language for Android development, and Kotlin has been used in the project for a long time. With the release of Kotlin1.3, Kotlin coroutines have been stable, and it is inevitable that there will be some thinking of their own.

For the network request function in the project, we are constantly reflecting on how to write it elegant, concise, fast, safe. I believe this is also a question that you developers are constantly thinking about. Since all of our projects use Retrofit as a network library, all of our thinking is based on Retrofit.

This article will start with my thinking about evolution. When it comes to Kotlin coroutines, extension methods, and DSLS, if you don’t have a basic partner, get to know these three things first, and I won’t cover them in this article. DSLS can watch me write this introduction

In network requests, the implicit problem we need to focus on is the binding of the page lifecycle, which requires closing the outstanding network request after closing the page. To this end, predecessors, is the eight immortals across the sea, each show their abilities. I also changed from learning and imitating my predecessors to self-understanding.

1. Callback

The Callback asynchronous method is the basic way to use Retrofit, as follows:

Interface:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String.@Field("pwd") pwd: String): Call<String>
}
Copy the code

Use:

val retrofit = Retrofit.Builder()
    .baseUrl("https://baidu.com")
    .client(okHttpClient.build())
    .build()

val api = retrofit.create(DemoService::class.java)
val loginService = api.login("1"."1")
loginService.enqueue(object : Callback<String> {
    override fun onFailure(call: Call<String>, t: Throwable){}override fun onResponse(call: Call<String>, response: Response<String>){}})Copy the code

I won’t go into details here.

To close a network request, call the cancel method in onDestroy:

override fun onDestroy(a) {
    super.onDestroy()
    loginService.cancel()
}
Copy the code

In this way, it is easy to forget to call the cancel method, and the network operation is separated from the operation that closes the request, which is not good for management.

This is certainly not the elegant approach. With the popularity of Rx, the network request method of our project has gradually changed to Rx

2. RxJava

This way of use, Baidu, everywhere is a tutorial, it can be seen that this way is at least a more recognized solution.

In the use of Rx, we also tried various encapsulation methods, such as customizing Subscriber, splitting and combining onNext, onCompleted and onError to meet different requirements.

First add Rx in the Retrofit converter RxJava2CallAdapterFactory. The create () :

addCallAdapterFactory(RxJava2CallAdapterFactory.create())
Copy the code

To use RxJava, change the interface Call to Observable:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String.@Field("pwd") pwd: String): Observable<String>
}
Copy the code

Use :(with RxAndroid binding declaration cycle)

api.login("1"."1")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread()) //RxAndroid
    .subscribe(object :Observer<String> {
        override fun onSubscribe(d: Disposable){}override fun onComplete(a){}override fun onNext(t: String){}override fun onError(e: Throwable){}})Copy the code

It’s a lot easier to use, and the idea of reactive programming is great, where everything is a flow of events. RxAndroid switches the UI thread and binds the page lifecycle, automatically cutting off the flow of events passing down when the page closes.

RxJava’s biggest risk is memory leaks, and RxAndroid does avoid some of those leaks. And by looking at the RxJava2CallAdapterFactory source, found that also did call cancel method, um… That sounds good. But I always felt that RxJava was too big and overqualified.

3. LiveData

With the progress of the project and the release of the Whole Google bucket. A lightweight version of RxJava has come into our sight, that is LiveData, LiveData borrowed a lot of RxJava design ideas, also belongs to the category of responsive programming. The biggest advantage of LiveData is that it responds to the Acitivty lifecycle without having to bind the declaration cycle as RxJava does.

Equally, the first thing we need to add LiveDataCallAdapterFactory (the link is the official Google to provide method and can be directly copied to the project), used for the retrofit of the Callback into LiveData:

addCallAdapterFactory(LiveDataCallAdapterFactory.create())
Copy the code

Change the interface to:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String.@Field("pwd") pwd: String): LiveData<String>
}
Copy the code

Call:

api.login("1"."1").observe(this, Observer {string ->
    
})
Copy the code

This is the most basic way to use it. When used in a project, it is common to customize an Observer to distinguish between different kinds of data.

In the previous call to the observe method, we pass a this, which refers to the declaration period, which we normally pass itself when we use it in AppCompatActivity.

The following is a simple jump to the source code. By looking at the source code, you can find:

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer)
Copy the code

The this itself is the passing LifecycleOwner.

If we jump on AppCompatActivity at any given level, we find that AppCompatActivity is a parent class that inherits from SupportActivity:

public class SupportActivity extends Activity implements LifecycleOwner.Component
Copy the code

It implements the LifecycleOwner interface itself. That is, unless otherwise requested, we only need to pass it on itself. LiveData automatically handles listening and unbinding data streams.

In general: data is bound once in onCreate, and then it is not bound again.

When the life cycle reaches onStart and onResume, LiveData automatically receives the event stream;

When the page is inactive, the stream of receiving events is paused, and data reception resumes when the page recovers. For example, if A jumps to B, then A will pause receiving. When returning from B to A, data stream reception will resume.

When the page is onDestroy, the observer is automatically deleted, thus interrupting the stream of events.

As you can see, LiveData, as the official suite, is simple to use and its lifecycle response is intelligent, generally requiring no additional processing.

(more advanced usage, you can refer to the official Demo, can be on the database cache waiting for a set of responsive encapsulation, very nice. Suggested to learn the official packaging thought, even if not, but also for their own benefits)

4. The Kotlin coroutines

With all that said, this is where we get down to business. If you take a closer look, you’ll see that you’re using the asynchronous enqueue method of Retrofit, followed by the network Callback method. Even the RxJava and Livedata converters are internally using the Callback method. Prior to this, the Retrofit authors also wrote a coroutine converter that had the address here, but still used the Callback internally, which is essentially the same. Retrofit supports suspend functions directly from Kotlin coroutines in the latest 2.6.0 release.

Those of you who have known about Retrofit before should know that Retrofit can be called in both synchronous and asynchronous ways.

void enqueue(Callback<T> callback);
Copy the code

This is the asynchronous way of calling, passing in a Callback, and this is the way we use it most often.

Response<T> execute(a) throws IOException;
Copy the code

The above method is a synchronous call method, will block the thread, return directly is the network data Response, rarely used.

Later, I was thinking about whether I could combine kotlin’s coroutine, abandon Callback, and directly use Retrofit’s synchronous method, and write asynchronous as synchronous. Code is written in sequence, with clear logic and high efficiency. Synchronous writing is more convenient for object management.

No sooner said than done.

First write an extension method to the coroutine:

valAPI =...fun <ResultType> CoroutineScope.retrofit(a) {
    this.launch(Dispatchers.Main) {
        val work = async(Dispatchers.IO) {
            try {
                api.execute() // Call the synchronous method
            } catch (e: ConnectException) {
                e.logE()
                println("Network connection error")
                null
            } catch (e: IOException) {
                println("Unknown network error")
                null
            }
        }
        work.invokeOnCompletion { _ ->
            // Cancel the task when the coroutine closes
            if (work.isCancelled) {
                api.cancel() // Call Retrofit's cancel method to close the network}}val response = work.await() // Wait for the IO task to complete and return data before continuing the following coderesponse? .let {if (response.isSuccessful) {
                println(response.body()) // The network request succeeded and the data was obtained
            } else {
                // Handle the HTTP code
                when (response.code()) {
                    401 -> {
                    }
                    500 -> {
                        println("Internal server error")
                    }
                }
                println(response.errorBody()) // The network request failed and the data was obtained}}}}Copy the code

Above is the core code, the main meaning is written comments. The whole working process is from the UI coroutines, so are free to operate UI controls, then go to synchronous calls in IO thread in network request, and wait for the IO thread is done, then get the result, in the writing of the entire process is based on the synchronization code, step in a process, not back off fracture are caused by the code. So let’s go ahead and try to get the data back out.

Here we take the DSL approach and first define a custom class:

class RetrofitCoroutineDsl<ResultType> {
    var api: (Call<ResultType>)? = null

    internal var onSuccess: ((ResultType?) -> Unit)? = null
        private set
    internal var onComplete: (() -> Unit)? = null
        private set
    internal varonFailed: ((error: String? , code,Int) - >Unit)? = null
        private set

    var showFailedMsg = false

    internal fun clean(a) {
        onSuccess = null
        onComplete = null
        onFailed = null
    }

    fun onSuccess(block: (ResultType?). -> Unit) {
        this.onSuccess = block
    }

    fun onComplete(block: () -> Unit) {
        this.onComplete = block
    }

    fun onFailed(block: (error: String? , code,Int) -> Unit) {
        this.onFailed = block
    }

}
Copy the code

This class exposes three methods: onSuccess, onComplete, and onFailed, which are used to categorize returned data.

Next, we modify our core code to pass the method:

fun <ResultType> CoroutineScope.retrofit(
    dsl: RetrofitCoroutineDsl<ResultType>. () -> Unit // Pass the method, which one is needed, which one is passed
) {
    this.launch(Dispatchers.Main) {
        valretrofitCoroutine = RetrofitCoroutineDsl<ResultType>() retrofitCoroutine.dsl() retrofitCoroutine.api? .let { it ->val work = async(Dispatchers.IO) { // The IO thread executes
                try {
                    it.execute()
                } catch(e: ConnectException) { e.logE() retrofitCoroutine.onFailed? .invoke("Network connection error".- 100.)
                    null
                } catch(e: IOException) { retrofitCoroutine.onFailed? .invoke("Unknown network error".- 1)
                    null
                }
            }
            work.invokeOnCompletion { _ ->
                // Cancel the task when the coroutine closes
                if (work.isCancelled) {
                    it.cancel()
                    retrofitCoroutine.clean()
                }
            }
            valresponse = work.await() retrofitCoroutine.onComplete? .invoke() response? .let {if(response.isSuccessful) { retrofitCoroutine.onSuccess? .invoke(response.body()) }else {
                        // Handle the HTTP code
                        when (response.code()) {
                            401 -> {
                            }
                            500-> { } } retrofitCoroutine.onFailed? .invoke(response.errorBody(), response.code()) } } } } }Copy the code

If you only need onSuccess, pass only this method, instead of all three, use it as needed.

Usage:

First, we need to modify the activity according to kotlin’s official document:

abstract class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job / / define the job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job // Activity coroutine

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy(a) {
        super.onDestroy()
        job.cancel() // End all coroutine tasks when the page is closed}}Copy the code

If an Activity implements the CoroutineScope interface, it can get a coroutine and use it directly according to the current context.

Next comes the real use, which calls this extension method anywhere:

retrofit<String> {
    api = api.login("1"."1")

    onComplete {
    }

    onSuccess { str ->
    }

    onFailed { error, code ->
    }
}
Copy the code

Sometimes we only need to deal with the onSuccess case and don’t care about the other two. So just write:

retrofit<String> {
    api = api.login("1"."1")

    onSuccess { str ->
    }
}
Copy the code

Write what you need, and the code is pretty neat.

As you can see, we do not need to bind the lifetime of the network request separately. When the page is destroyed, the job is closed. When the coroutine is closed, the network is closed by calling Retrofit’s cancel method.

5. Section

Coroutine is less expensive than Thread and has fast response, which makes it very suitable for lightweight workflows. The use of coroutines also led me to think and learn more deeply. Coroutine is not a substitute for Thread, but a supplement for multiple asynchronous tasks. We should not understand coroutine according to conventional thinking, but develop more comfortable ways to use it based on its own characteristics. And with the release of Retrofit 2.6.0, the new coroutine solution comes with the following:

@GET("users/{id}")
suspend fun user(@Path("id") long id): User
Copy the code

The use of visible coroutines will become more popular with the addition of support for suspend functions.

All of the above network processing methods, whether Rx or LiveData, are good encapsulation methods, and there is no good or bad technology. My coroutine encapsulation method may not be the best, but we can not lack the three elements of thinking, exploration and practice, to think and do.

The best answer is always given by yourself.

This is the first time to write this type of article record, process is more serious, record is not rigorous, everyone forgive me. Thank you for reading