Retrofit supports Coroutines

Retrofit has been updated to version 2.6.0 with built-in support for Kotlin Coroutines, further simplifying the process of making network requests using Retrofit and Coroutines since 2019. Now that we’re halfway through 2020, the current version is 2.9.0, and Okhttp3 is 4.7.2, by the way

What does a normal network request require

In a Word document, draw a rough picture:

The Lifecycle LifecycleCoroutineScop

This class needs to introduce: androidx. Lifecycle: lifecycle – runtime – KTX: lastest version released by the end of this article, I find the latest version of the lastest version: 2.3.0 – alpha03 source code:

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */ val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope /** * [CoroutineScope] tied to this [Lifecycle]. * * This scope  will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] */ val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl? if (existing ! = null) { return existing } val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) if (mInternalScopeRef.compareAndSet(null, newScope)) { newScope.register() return newScope } } }Copy the code

Instead of looking at the code, let’s take a look at the comment and see the familiar CoroutineScope to ~! There’s also one on the coroutine ViewModel side, if you’re interested. Take a look at the LifecycleCoroutineScope class:

/** * [CoroutineScope] tied to a [Lifecycle] and * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] * * This scope will be cancelled when  the [Lifecycle] is destroyed. * * This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted], * [launchWhenResumed] */ abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope { internal abstract val lifecycle: Lifecycle /** * Launches and runs the given block when the [Lifecycle] controlling this * [LifecycleCoroutineScope] is at leastin [Lifecycle.State.CREATED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenCreated
     * @see Lifecycle.coroutineScope
     */
    fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenCreated(block)
    }

    /**
     * Launches and runs the given block when the [Lifecycle] controlling this
     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenStarted
     * @see Lifecycle.coroutineScope
     */

    fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenStarted(block)
    }

    /**
     * Launches and runs the given block when the [Lifecycle] controlling this
     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
     *
     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
     * @see Lifecycle.whenResumed
     * @see Lifecycle.coroutineScope
     */
    fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
        lifecycle.whenResumed(block)
    }
}
Copy the code

Then we noticed that we all want web requests to end themselves in the Destroy method of an Activity or Fragment, The returned [Job] will be cancelled when The [Lifecycle] is destroyed. Where launchWhenCreated and launchWhenStarted are the methods we pay attention to, and the state is here:

  /**
         * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
         *     <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
         * </ul>
         */
        CREATED,

        /**
         * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onStart() onStart} call;
         *     <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
         * </ul>
         */
        STARTED,
Copy the code

It can be chosen according to individual needs, and my preferred solution is launchWhenCreated for subsequent development.

Encapsulating network requests

Before we begin encapsulation, we define an interface

interface JsonResult {
    fun isLegal() : Boolean
}
Copy the code

IsLegal () is used to determine whether the network interface is returning the correct returned content, which has the advantage of easily adapting to the data structure returned by the variable interface. The following implementation uses amap’s weather interface as a reference: Create a new base class that inherits this interface

open class JsonData : JsonResult {

    var status = ""

    override fun isLegal(): Boolean = status == "1"

}
Copy the code

Then we create a new class that inherits the base class

data class Weather(@SerializedName("lives")
                   val lives: ArrayList<LivesItem>,
                   @SerializedName("count")
                   val count: String = "",
                   @SerializedName("infocode")
                   val infocode: String = "",
                   @SerializedName("info")
                   val info: String = "") : JsonData()
Copy the code

The base class and the parse class need to be adjusted according to your interface. Amap interface returns something like this:

{"status":"1"."count":"1"."info":"OK"."infocode":"10000"."lives": [{"province":"Guangdong"."city":"Baiyun District"."adcode":"440111"."weather":"Rain"."temperature":"23"."winddirection":"The west"."windpower":"3" or less."humidity":"98"."reporttime":"The 2020-06-09 22:52:20"}}]Copy the code

The basic information is known, because the development now generally began to use LiveData for distribution, then we need to process each state in the request process, first look at the code:

class KResult<T>(var status:String, var response: T? = null){

    companion object{

        const val Loading = "Loading"
        const val Success = "Success"
        const val Failure = "Failure"
        const val OutTime = "OutTime"} private var loading:(()->Unit)? = null fun whenLoading(loading:()->Unit){ this.loading = loading } private var success:(T? .()->Unit)? = null fun whenSuccess(success:(T? .()->Unit)){ this.success = success } private var failed:(T? .()->Unit)? = null fun whenFailed(failed:(T? .()->Unit)){ this.failed = failed } private var outTime:(()->Unit)? = null fun whenOutTime(outTime:()->Unit){ this.outTime = outTime } fun whenStatus(result: KResult<T>.()->Unit){ result.invoke(this) when(status){ Loading ->{ loading? .invoke() } Success ->{ success? .invoke(response) } Failure ->{ failed? .invoke(response) } OutTime ->{ outTime? .invoke() } } } }Copy the code

So combined with the above, we can encapsulate the network as follows:

class CallResult<T: JsonResult> constructor(private var owner: LifecycleOwner? , callResult: CallResult<T>.()->Unit) { init { callResult() } private var data : MutableLiveData<KResult<T>>? = null var repeatWhenOutTime = 0 private var tryCount = 0 fun post(data : MutableLiveData<KResult<T>>){ this.data = data } fun hold(result:suspend() -> Response<T>){ var response: Response<T>? var netJob: Job? = null var call : KResult<T> owner? .apply { netJob = lifecycleScope.launchWhenCreated { call = KResult(KResult.Loading) makeCall(call) onLoading? .invoke() response = tryRepeat(result)if(response ! = null) { response? .run {if(code() ! = 200){ call = KResult(KResult.OutTime) makeCall(call) loadingOutTime? .invoke(call) netJob? .cancel() }else{
                            build(response)
                        }
                    }
                } else{ call = KResult(KResult.OutTime) makeCall(call) loadingOutTime? .invoke(call) netJob? .cancel() } } } } privatesuspend fun tryRepeat(result: suspend() -> Response<T>) : Response<T>? {return try {
            val output = withTimeoutOrNull(10000){
                withContext(Dispatchers.IO){
                    result.invoke()
                }
            }
            Logger.eLog("CallResult"."output : $output  tryCount : $tryCount")
            return if(output == null && tryCount < repeatWhenOutTime){
                tryCount ++
                tryRepeat(result)
            }else{ output } }catch (e:java.lang.Exception){ e.printStackTrace() null } } private fun build(response: Response<T>?) { response? .apply { val call : KResult<T> try {if(body() == null) { call = KResult(KResult.Failure, null) makeCall(call) onError? .invoke(call) }else {
                    if(body()!! .islegal ()) {call = KResult(kresult.success, body()) makeCall(call) onSuccess? .invoke(call) }else{ call = KResult(KResult.Failure, body()) makeCall(call) onError? .invoke(call) } } } catch (e: Exception) { e.printStackTrace() } } } private fun makeCall(call: KResult<T>) : KResult<T> { data? .postValue(call)return call
    }

    private var onLoading: (() -> Unit)? = null

    fun loading(onLoading: (() -> Unit)): CallResult<T> {
        this.onLoading = onLoading
        return this
    }

    private var loadingOutTime: ((result: KResult<T>) -> Unit)? = null
    fun outTime(loadingOutTime: ((result: KResult<T>) -> Unit)) : CallResult<T> {
        this.loadingOutTime = loadingOutTime
        return this
    }


    private var onSuccess: ((result: KResult<T>) -> Unit)? = null

    private var onError: ((result: KResult<T>) -> Unit)? = null

    fun success(onSuccess: ((result: KResult<T>) -> Unit)): CallResult<T> {
        this.onSuccess = onSuccess
        return this
    }


    fun error(onError: ((result: KResult<T>) -> Unit)): CallResult<T> {
        this.onError = onError
        return this
    }


}
Copy the code

An example which used to DSL grammar, forwarder will, first of all focus: lifecycleScope. LaunchWhenCreated invokes the withContext (Dispatchers. The Main) according to the source code:

suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspendCoroutineScope.() -> T ) = withContext(Dispatchers.Main.immediate) { val job = coroutineContext[Job] ? : error("when[State] methods should have a parent job")
    val dispatcher = PausingDispatcher()
    val controller =
        LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
    try {
        withContext(dispatcher, block)
    } finally {
        controller.finish()
    }
}

Copy the code

A, used here withContext (Dispatchers. Main. Immediate), so I here withContext (Dispatchers. The Main) if there is any need to write so much? What are the two differences? One question for you to think about. Two, the second one is withTimeoutOrNull

Runs a given suspending block of code inside 
a coroutine with a specified [timeout][timeMillis] 
and returns `null` if this timeout was exceeded.
Copy the code

If a timeout occurs, the result is null.

How to use

The corresponding interface for Retrofit, plus suspend

 @GET("/v3/weather/weatherInfo")
   suspend fun getWeather(@Query("key") key:String,@Query("city") city:String) : Response<Weather>
   
Copy the code

Where the network request is executed, the following:

fun getWeather(code:String,call:MutableLiveData<KResult<Weather>>){
        CallResult<Weather>(owner){
            post(call)
            hold {  main.getWeather("XXXXXXXXXXXXX",code) }
        }

    }
Copy the code

Where Weather corresponds to LiveData is accepted as follows:

weather.observe(this, Observer { it? .apply {whenStatus {whenLoading {// process loading} whenSuccess {// process loading}.apply {whenStatus {whenLoading {// process loading} whenSuccess {// Process loading} } whenFailed {// whenOutTime {// whenOutTime}}}})Copy the code

conclusion

Kotlin’s Coroutines, which are not very deep, are further encapsulation of thread pools to implement scheduling and so on, and you can see three obvious advantages: First, it’s easy to leave the main thread. See RxJava, where coroutines allow developers to do this with less code and no tedious callbacks. Second, the least boilerplate code. Coroutines fully leverage the capabilities of the Kotlin language, including the suspend method. Make writing thread tasks similar to writing regular blocks of code. When an operation no longer needs to be performed, times out, or is affected by a different state of the LifeOwner, the coroutine cancels it automatically.