Kotlin Coroutine series: 3. Android uses Kotlin Coroutine and Retrofit for network requests and cancellation requests

While the previous two articles covered some of the basic concepts and basics of coroutines, this article introduces how to initiate network requests using coroutines in Conjunction with Retrofit on Android, and how to gracefully cancel existing network requests when using coroutines.

This article the Demo address: https://github.com/huyongli/AndroidKotlinCoroutine

Create CoroutineScope

In a previous article I wrote that the coroutinescope.launch method is a popular coroutine builder. To use coroutines, you must first create a CoroutineScope object, as follows:

CoroutineScope(Dispatchers.Main + Job())
Copy the code

The above code creates a CoroutineScope object, assigns its coroutine to be executed in the main thread, and assigns a Job

In the demo I used MVP mode to write, so I put the CoroutineScope creation into BasePresenter, the code is as follows:

interface MvpView

interface MvpPresenter<V: MvpView> {

    @UiThread
    fun attachView(view: V)

    @UiThread
    fun detachView(a)
}

open class BasePresenter<V: MvpView> : MvpPresenter<V> {
    lateinit var view: V
    val presenterScope: CoroutineScope by lazy {
        CoroutineScope(Dispatchers.Main + Job())
    }

    override fun attachView(view: V) {
        this.view = view
    }

    override fun detachView(a) {
        presenterScope.cancel()
    }
}
Copy the code

Use coroutinescope.cancel () to retrieve the cancel routine

Presenterscope.cancel () is called from basePrespresenterScope. DetachView. This method cancels all coroutines created by presenterScope and its children.

I also mentioned in the previous article that when launching a coroutine, a Job object is returned. You can also cancel the coroutine corresponding to the task by using the Job object’s Cancel method. So why don’t I use this method here?

Cancel () is a more complicated way to create each coroutine and then cancel it. Coroutinescope.cancel () cancels all coroutines and subcoroutines created by the coroutine context at once. This code can also be easily extracted into the base class, so that later in the business code is not concerned with coroutines and View life cycle issues.

Coroutinescope.cancel () ends up using job.cancel ()

Extend retrofit. Call adaptation coroutine

interface ApiService {
    @GET("data/iOS/2/1")
    fun getIOSGank(a): Call<GankResult>

    @GET("data/Android/2/1")
    fun getAndroidGank(a): Call<GankResult>
}

class ApiSource {
    companion object {
        @JvmField
        val instance = Retrofit.Builder()
            .baseUrl("http://gank.io/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .build().create(ApiService::class.java)}}Copy the code

As you can see from the API definition above, which should be familiar, we can initiate an asynchronous network request using the following code

ApiSource.instance.getAndroidGank().enqueue(object : Callback<T> {
    override fun onFailure(call: Call<T>, t: Throwable){}override fun onResponse(call: Call<T>, response: Response<T>){}})Copy the code

Previous articles have shown that coroutines can make asynchronous code as convenient as writing synchronous code. Can the above asynchronous code be modified to look like writing synchronous code blocks using coroutines? Obviously it can, the specific transformation code is as follows:

// Extend the Retrofit.call class by extending an await method and identifying it as a pending function
suspend fun <T> Call<T>.await(a): T {
    return suspendCoroutine {
        enqueue(object : Callback<T> {
            override fun onFailure(call: Call<T>, t: Throwable) {
                // The request fails, an exception is thrown, and the current coroutine is terminated manually
                it.resumeWithException(t)
            }

            override fun onResponse(call: Call<T>, response: Response<T>) {
                if(response.isSuccessful) {
                   // The request is successful. Retrieve the request result and manually restore the coroutine
                   it.resume(response.body()!!)
                } else{
                   // Request status is abnormal, throw an exception, manually terminate the current coroutine
                   it.resumeWithException(Throwable(response.toString()))
                }
            }
        })
    }
}
Copy the code

The above code extends a pending function await, which, when executed, will execute an asynchronous request for retrofit.call and suspend the function in the coroutine until the asynchronous request succeeds or fails and then resumes the coroutine.

suspendCoroutine

A global function that gets the coroutine context of the current method and suspends the current coroutine until it resumes execution at some point, but this timing is controlled by the developer itself, as in the code above. Resume and IT. ResumeWithException.

To initiate a request, write method 1

// Use coroutinescope.launch to create a coroutine that is executed in the main thread
presenterScope.launch {
    val time = System.currentTimeMillis()
    view.showLoadingView()
    try {
        val ganks = queryGanks()
        view.showLoadingSuccessView(ganks)
    } catch (e: Throwable) {
        view.showLoadingErrorView()
    } finally {
        Log.d(TAG, "Time consuming:${System.currentTimeMillis() - time}")
    }
}

suspend fun queryGanks(a): List<Gank> {
    // This method is executed on the same thread as the caller and is therefore executed on the main thread
    return try {
        // Query the Android list first, while the current coroutine execution process is suspended here
        val androidResult = ApiSource.instance.getAndroidGank().await()
        
        // Resume the current coroutine after the Android list query is complete, then query the IOS list, and suspend the current coroutine execution process here
        val iosResult = ApiSource.instance.getIOSGank().await()

        // After the query is complete, restore the coroutine and merge the results of the Android and IOS lists. The query is complete
        val result = mutableListOf<Gank>().apply {
            addAll(iosResult.results)
            addAll(androidResult.results)
        }
        result
    } catch (e: Throwable) {
        // Handle exceptions in coroutines, otherwise the program will crash
        e.printStackTrace()
        throw e
    }
}
Copy the code

As you can see from the code above, exceptions in coroutines are handled in try-catch mode, and that’s all I’m thinking about right now. So when using coroutines, it is best to use try-catch in the right place in the business to catch exceptions, otherwise the program will crash as soon as an exception occurs during coroutine execution.

The queryGanks() method takes the time of two network requests because the Android list and the ios list are not in parallel. So this is definitely not an optimal solution.

Make a request, notation 2

So let’s write it another way.

suspend fun queryGanks(a): List<Gank> {
    /** * This method is executed in the main thread because the network request itself is an asynchronous request and async must be executed in the context of the coroutine, so this method implementation uses withContext to switch the execution thread to the main thread and get the coroutine context object */
    return withContext(Dispatchers.Main) {
        try {
            // Create a new coroutine in the current coroutine to initiate an Android list request, but not suspend the current coroutine
            val androidDeferred = async {
                val androidResult = ApiSource.instance.getAndroidGank().await()
                androidResult
            }

            // Immediately after the Android list request is initiated, another child coroutine is created in the current coroutine to initiate the ios list request without suspending the current coroutine
            val iosDeferred = async {
                val iosResult = ApiSource.instance.getIOSGank().await()
                iosResult
            }

            val androidResult = androidDeferred.await().results
            val iosResult = iosDeferred.await().results

            // The two list requests are executed in parallel, waiting for the two requests to end and merging the results of the requests
            // The execution time of the current method is actually the longest of the two requests, not the sum of the two requests, so this method is better than the above method
            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
Copy the code

The difference between this method and the previous one is that the async builder creates two sub-coroutines to request the Android list and IOS list respectively. At the same time, because the async builder does not suspend the current coroutine during execution, the two requests are executed in parallel, so the efficiency is much higher than the previous method.

Make a request, notation 3

The third method is to implement a custom CallAdapterFactory in Retorfit, and convert the result of API definition Call to Deferred. In this way, Android list request and IOS list request can be launched simultaneously. We then get the request result with deferred.await, which is a combination of notation one and notation two.

This is already done by JakeWharton at github.com/JakeWharton…

Here I will not say the concrete implementation of this scheme, interested students can go to see its source code.

The specific code of writing method 3 is as follows:

val instance = Retrofit.Builder()
        .baseUrl("http://gank.io/api/")
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(CallAdapterApiService::class.java)
        
suspend fun queryGanks(a): List<Gank> {
    return withContext(Dispatchers.Main) {
        try {
            val androidDeferred = ApiSource.callAdapterInstance.getAndroidGank()

            val iosDeferred = ApiSource.callAdapterInstance.getIOSGank()

            val androidResult = androidDeferred.await().results

            val iosResult = iosDeferred.await().results

            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
Copy the code

The third option, which seems more concise, is also a parallel request, taking the time of the longest request, which is similar to the second option.

Specific implementation of demo address see the beginning of the article, interested can see.