Android开发:协程Coroutines&LifecycleCoroutineScop&Retrofit网络请求

Retrofit支持Coroutines

Retrofit 自从更新到了 2.6.0 版本,内置了对 Kotlin Coroutines 的支持,进一步简化了使用 Retrofit 和协程来进行网络请求的过程,这已经是2019年的事情。</br>
现在2020年已经到了一半了,目前版本已经到了2.9.0了,顺带一提Okhttp3也到了4.7.2

一般的网络请求需要什么

在Word文档里,大致画一下图:


image

从图片可以看出我们其实不仅需要除妖处理数据返回正确与否,其实还需各种状态去判断,当前请求的情况,所以我们需要引入状态这个因素进入网络请求框架。

Lifecycle中的LifecycleCoroutineScop

这个类需要引入:androidx.lifecycle:lifecycle-runtime-ktx: lastest version
截至本文章发布,我找到的最新版本lastest version: 2.3.0-alpha03
</br>
源码:

/**
 * [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
            }
        }
    }

先不看代码,我们看一下那个注释,看到熟悉的CoroutineScope 对~!协程
ViewModel那边也有一个,有兴趣的同学可以去看看。再看看LifecycleCoroutineScope这个类:

/**
 * [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 least in [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)
    }
}

然后我们关注到一点,我们都希望网络请求能在Activity or Fragmentdestroy方法中自己结束,看看注释The returned [Job] will be cancelled when the [Lifecycle] is destroyed.,帮我们完成了。其中launchWhenCreatedlaunchWhenStarted是我们关注的方法,这个state在这里:

  /**
         * 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,

可以根据个人需求选择,我的选择的方案是launchWhenCreated进行后续开发。

封装网络请求

在开展封装前,我们先定义一个接口

interface JsonResult {
    fun isLegal() : Boolean
}

isLegal()用于判断网络接口返回是否为正确的返回内容,这么做的好处是方便适配多变的接口返回的数据结构。</br>
以下开展以高德地图的天气接口作为参考:
新建一个基础类继承这接口

open class JsonData : JsonResult {

    var status = ""

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

}

然后我们再新建一个类去继承这个基础类

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()

这个类基础类和后面这个解析类需要根据你接口进行调整,高德地图的接口返回是这样的:

{"status":"1","count":"1","info":"OK","infocode":"10000","lives":[{"province":"广东","city":"白云区","adcode":"440111","weather":"雨","temperature":"23","winddirection":"西","windpower":"≤3","humidity":"98","reporttime":"2020-06-09 22:52:20"}]}

基本的信息都知道了,由于开发现在普遍都开始利用了LiveData进行分发的,那我们需要进行请求过程中各个状态进行处理,先看代码:

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()
            }
        }
    }

}

所以结合以上这些,我们就能把网络封装写成如下:

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()
                }
            }
        }
    }

    private suspend 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
    }


}

其中利用到DSL语法,待会举例子,首先关注点:
lifecycleScope.launchWhenCreated 中调用这个withContext(Dispatchers.Main)
根据源码:

suspend fun <T> Lifecycle.whenStateAtLeast(
    minState: Lifecycle.State,
    block: suspend CoroutineScope.() -> 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()
    }
}

一、这里使用了withContext(Dispatchers.Main.immediate),所以我这里withContext(Dispatchers.Main)有没有必要这么写呢?两个区别在哪里?留个问题给大家思考</br>
二、第二个是withTimeoutOrNull这个方法,源码注释

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

简单理解就是超时时候返回结果null,方便我们判断是否超时。

如何使用

Retrofit对应的接口,加上suspend

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

在执行网络请求的地方,如下:

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

    }

在接受Weather对应LiveData的地方如下:

weather.observe(this, Observer {
            it?.apply {
                whenStatus {
                    whenLoading { 
                        //处理加载
                    }
                    whenSuccess {
                        //处理成功 这里是this不是it 对应 Weather?
                    }
                    whenFailed { 
                        //处理失败
                    }
                    whenOutTime { 
                       //处理超时
                    }
                }
            }
        })

总结

Kotlin的协程Coroutines,并非很深的内容,属于对线程池的进一步封装实现调度等等
可以看到很明显的三个优点:</br>
一、很容易离开主线程,参考RxJava,对比下协程可以让开发者只需要更少的代码就完成这个工作,而且没有累人的回调处理。
二、样板代码最少。协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。让编写线程任务过程就和编写普通的代码块差不多。
三、当一个操作不再需要被执行时或者超时或者受LifeOwner的不同状态影响,协程会自动取消它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345