超简单-用协程简化你的网络请求吧,兼容你的老项目和旧的网络请求方式

·  阅读 730

前言

在Kotlin协程(后简称协程)出来之后,颠覆了我们很多工具类的封装方式,大大简化了我们很多api的调用,并且使异步操作逻辑更清晰了

其中一个很标志性的地方就属网络请求了,以前的网络请求方式声明很麻烦,请求和响应也很麻烦,总结一句话就是啰嗦且易出错

ps:最终的使用方式示例:

使用网络请求的进化过程:

1.从一开始的HttpURLConnection的一把梭,直接一个请求写一串代码(代码太多就不写了)

2.到后续使用三方封装的网络框架,如XUtils,Volly,OkHttp等,这些虽然也简化了操作,但也是很复杂,比如封装了两层的OkHttp可能还需要以下方式声明和调用:

然后在一个或多个回调中处理返回的数据并判断是否是请求成功,解析成响应的数据并操作

3.后来Retrofit出现,解救了苦于网络请求太啰嗦的万千开发们,其使用动态代理加良好的封装,使网络请求声明和请求都变得更简单了

ps:Retrofit如何使用动态代理参考:模仿Retrofit封装一个使用更简单的网络请求框架_lt的博客-CSDN博客_retrofit封装网络请求

pps:进一步简化Retrofit的声明参考:更易于使用的Retrofit(不用写注解)_lt的博客-CSDN博客

声明如下(其会在调用的时候自动解析成一个post请求的Call对象):

使用如下:

可以发现虽然声明简单了很多,但使用还是比较麻烦

这时候就轮到协程登场了

正文

如果是使用Retrofit配合协程简化网络请求参考我以前写的文章第二条(封装网络请求):Kotlin协程在项目中的实际应用_lt的博客-CSDN博客_kotlin协程使用

否则可以看下面的:

接下来就是重头戏:用协程兼容你的老项目和旧的网络请求方式,并且请求方式就像这张图一样

首先我们可以观察一下我们的旧方式的请求,发现可能是有一个统一的回调,将服务端返回的String返回来,可能类似情况1:

也可能使用OkHttp,类似情况2:

也可能类似情况3:

可以观察到,他们都是基于回调,而协程可以将回调转成同步的挂起函数,所以我们将第三种方式(其他情况也类似),改造代码如下:

interface BaseHttp {
    //网络请求成功后调用该方法
    fun onHttpSuccess(data: String?)

    //网络请求失败后调用该方法
    fun onHttpFailed(message: String?)
}

/**
 * creator: lt  lt.dygzs@qq.com
 * effect : 使用协程封装基于回调的网络请求
 * warning:
 */
abstract class CoroutineHttp<T : Any> : BaseHttp {
    private var continuation: CancellableContinuation<Result>? = null//协程对象,如果getX调用慢于回调用
    private var result: Result? = null//响应结果,如果getX先与回调用

    //网络请求成功后调用该方法
//2*********************************
    override fun onHttpSuccess(data: String?) {
        //保证线程安全
        handler.post {
            val continuation = continuation
            if (continuation == null) {
                result = Result(data, true)
                return@post
            }
            continuation.resume(Result(data, true))
        }
    }

    //网络请求失败后调用该方法
    override fun onHttpFailed(message: String?) {
        handler.post {
            val continuation = continuation
            if (continuation == null) {
                result = Result(message, false)
                return@post
            }
            continuation.resume(Result(message, false))
        }
    }

    /**
     * 检查协程网络请求的返回值和当前状态是否符合要求
     * 如果不符合要求,会回调失败的接口,并返回null,不会取消当前协程
     * [errorListener]msg:服务端返回的消息或异常信息,如果不传使用默认失败策略(即弹toast)
     */
//1*********************************
    suspend fun getOrNull(errorListener: (msg: String?) -> Unit = HttpErrorListener): T? {
        //保证线程安全
        val json = withContext(Dispatchers.Main) {
            val result = suspendCancellableCoroutine<Result> { continuation ->
                val result = result
                if (result != null) {
//4*********************************
                    continuation.resume(result)
                    return@suspendCancellableCoroutine
                }
                this@CoroutineHttp.continuation = continuation
            }
//3*********************************
            return@withContext if (result.isSuccess) result.data else {
                errorListener(result.data)
                null
            }
        }
        return withContext(Dispatchers.Default) {
            Gson().fromJson(
                json,
                this@CoroutineHttp.getSuperClassTypes().first()
            )
        }
    }


    /**
     * 检查协程网络请求的返回值和当前状态是否符合要求
     * 如果不符合要求,会回调失败的接口,并取消当前协程
     * [errorListener]msg:服务端返回的消息或异常信息,如果不传使用默认失败策略(即弹toast)
     */
    suspend fun get(errorListener: (String?) -> Unit = HttpErrorListener): T =
        getOrNull(errorListener) ?: cancelAndThrow()

    //获取协程数据,并自动显示和隐藏弹窗,并自动转换成响应类型,如果请求失败或转换失败则返回null
    suspend fun getOrNullWithDialog(errorListener: (String?) -> Unit = HttpErrorListener): T? {
        val baseActive = coroutineContext[CoroutineElementWithBaseActive]?.baseActive
        baseActive?.showWaitDialog()
        try {
            return getOrNull(errorListener)
        } finally {
            baseActive?.dismissWaitDialog()
        }
    }

    //    获取协程数据,并自动显示和隐藏弹窗,并自动转换成响应类型,如果请求失败或转换失败则取消协程
    suspend fun getWithDialog(errorListener: (String?) -> Unit = HttpErrorListener): T =
        getOrNullWithDialog(errorListener) ?: cancelAndThrow()

    class Result(val data: String?, val isSuccess: Boolean)

    private object HttpErrorListener : (String?) -> Unit {
        override fun invoke(p1: String?) {
            p1.showToast()
        }
    }

    /**
     * 取消并抛出异常
     */
    suspend fun cancelAndThrow(): Nothing = coroutineContext.job.cancelAndThrow()

/**
 * 获取super的泛型的type列表
 */
fun Any.getSuperClassTypes(): Array<Type> =
    this::class.java
        .genericSuperclass
        .asT<ParameterizedType>()
        .actualTypeArguments
}

private val handler = Handler(Looper.getMainLooper())
复制代码

原理大致就是将协程内部的回调给保存下来(挂起),然后在适当的时间调用协程的resume使协程恢复执行

可以看代码标识1处,其他的getxxx方法都会走到这个getOrNull方法中,其先调用suspendCancellableCoroutine方法将协程挂起并拿到内部的回调

然后再看代码标识2处,网络请求回调成功后会走这个onHttpSuccess方法,然后我们判断是否已经挂起了协程(注册了协程的回调):

1.如果挂起了,就调用resume恢复协程的运行,此时代码会执行到标识3处,且将result传了过去,然后后续就执行解析json并判断是否成功等耦合项目的逻辑

2.如果没有挂起,就将数据保存在类中,然后会在调用getxxx方法时检测到有数据并直接恢复协程(标识4处)

然后比如网络请求这样声明:

最后我们就可以使用协程请求网络了(红框的位置相当于getWithDialog方法的成功回调):

也可以在方法后面传入回调处理请求失败的情况(红框位置是成功的回调,黄框位置是失败的回调):

使用协程和gson解析需要导入以下的依赖:

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
    implementation 'com.google.code.gson:gson:2.8.7'
复制代码

扩展

1.CoroutineHttp为什么要声明为抽象类?

因为jvm的泛型擦除,如果你不创建出新的Class,就无法获取他设置的泛型的Type,而使用抽象类,你每new一个匿名内部类都会创建出一个Class,所以可以通过反射获取其声明的泛型的Type

当然open class也可以写出匿名内部类,但是没有了硬性规定容易出现遗漏导致出现bug

2.mainScope是什么

mainScope是一个自定义的协程作用域(CoroutineScope),创建它可以参考这一篇Kotlin-如何创建一个好用的协程作用域_lt的博客-CSDN博客

一般在activity中需要在onCreate中创建,在onDestroy中调用cancel()来取消它所有启动的协程

如果在fragment中就在onCreate中创建,在onDestroyView中调用cancel()

如果有疑问可以评论区留言

end

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改