百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

阿里P7大牛论:Kotlin协程优雅的与Retrofit缠绵

lipiwang 2024-11-01 14:10 9 浏览 0 评论

前言

Kotlin已经成为Android开发的Google第一推荐语言,项目中也已经使用了很长时间的kotlin了,加上Kotlin1.3的发布,kotlin协程也已经稳定了,难免会有一些自己的思考。

对于项目中的网络请求功能,我们也在不停的反思,如何将其写的优雅、简洁、快速、安全。相信这也是各位开发者在不停思考的问题。由于我们的项目都是使用的Retrofit作为网络库,所以,所有的思考都是基于Retrofit展开的。本篇文章中将会从我的思考进化历程开始讲起。涉及到Kotlin的协程、扩展方法、DSL,没有基础的小伙伴,先去了解这三样东西,本篇文章不再进行讲解。DSL可以看看我写这篇简介

在网络请求中,我们需要关注的隐式问题就是:页面生命周期的绑定,关闭页面后需要关闭未完成的网络请求。为此,各位前辈,是八仙过海、各显神通。我也是从学习、模仿前辈,到自我理解的转变。

1. Callback

在最初的学习使用中,Callback异步方法是Retrofit最基本的使用方式,如下:

接口:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): Call<String>
}

使用:

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

    }
})

这里不再细说。

在关闭网络请求的时候,需要在onDestroy中调用cancel方法:

override fun onDestroy() {
    super.onDestroy()
    loginService.cancel()
}

这种方式,容易导致忘记调用cancel方法,而且网络操作和关闭请求的操作是分开的,不利于管理。

这当然不是优雅的方法。随着Rx的火爆,我们项目的网络请求方式,也逐渐转为了Rx的方式

2. RxJava

此种使用方式,百度一下,到处都是教程讲解,可见此种方式起码是大家较为认可的一种方案。

在Rx的使用中,我们也尝试了各种各样的封装方式,例如自定义Subscriber,将onNext、onCompleted、onError进行拆分组合,满足不同的需求。首先在Retrofit里添加Rx转换器RxJava2CallAdapterFactory.create():

addCallAdapterFactory(RxJava2CallAdapterFactory.create())

RxJava的使用方式大体如下,先将接口的Call改为Observable:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): Observable<String>
}

使用:(配合RxAndroid绑定声明周期)

api.login("1","1")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread()) //RxAndroid
    .subscribe(object :Observer<String> {
        override fun onSubscribe(d: Disposable) {

        }

        override fun onComplete() {

        }

        override fun onNext(t: String) {

        }

        override fun onError(e: Throwable) {
        }
    })

这种使用方式确实方便了不少,响应式编程的思想也很优秀,一切皆为事件流。通过RxAndroid来切换UI线程和绑定页面生命周期,在页面关闭的时候,自动切断向下传递的事件流。

RxJava最大的风险即在于内存泄露,而RxAndroid确实规避了一定的泄露风险。并且通过查看RxJava2CallAdapterFactory的源码,发现也确实调用了cancel方法,嗯……貌似不错呢。

但总是觉得RxJava过于庞大,有些大材小用。

3. LiveData

随着项目的的推进和Google全家桶的发布。一个轻量化版本的RxJava进入到了我们视线,那就是LiveData,LiveData借鉴了很多RxJava的的设计思想,也是属于响应式编程的范畴。LiveData的最大优势即在于响应Acitivty的生命周期,不用像RxJava再去绑定声明周期。

同样的,我们首先需要添加LiveDataCallAdapterFactory (链接里是google官方提供的写法,可直接拷贝到项目中),用于把retrofit的Callback转换为LiveData:

addCallAdapterFactory(LiveDataCallAdapterFactory.create())

接口改为:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): LiveData<String>
}

调用:

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

以上就是最基础的使用方式,在项目中使用时候,通常会自定义Observer,用来将各种数据进行区分。

在上面调用的observe方法中,我们传递了一个this,这个this指的是声明周期,一般我们在AppCompatActivity中使用时,直接传递其本身就可以了。

下面简单跳转源码进行说明下。通过查看源码可以发现:

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

其this本身是传递的LifecycleOwner。

那么我们在一层层跳转AppCompatActivity,会发现AppCompatActivity是继承于SupportActivity的父类:

public class SupportActivity extends Activity implements LifecycleOwner, Component

其本身对LifecycleOwner接口进行了实现。也就是说,除非特殊要求,一般我们只需要传递其本身就可以了。LiveData会自动处理数据流的监听和解除绑定。

通常来说:在onCreate中对数据进行一次性的绑定,后面就不需要再次绑定了。

当生命周期走到onStart和onResume的时候,LiveData会自动接收事件流;当页面处于不活动的时候,将会暂停接收事件流,页面恢复时恢复数据接收。(例如A跳转到B,那么A将会暂停接收。当从B回到A以后,将恢复数据流接收) 当页面onDestroy时候,会自动删除观察者,从而中断事件流。

可以看出LiveData作为官方套件,使用简单,生命周期的响应也是很智能的,一般都不需要额外处理了。

(更高级的用法,可以参考官方Demo,可以对数据库缓存等待都进行一整套的响应式封装,非常nice。建议学习下官方的封装思想,就算不用,也是对自己大有裨益)

4. Kotlin协程

上面说了那么多,这里步入了正题。大家仔细观察下会发现,上面均是使用的Retrofit的enqueue异步方法,再使用Callback进行的网络回调,就算是RxJava和Livedata的转换器,内部其实也是使用的Callback。在此之前,Retrofit的作者也写了一个协程的转换器,地址在这:https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter,但内部依然使用的是Callback,本质均为一样。(目前该库才被废弃,其实我也觉得这样使用协程就没意义了,Retrofit在最新的2.6.0版本,直接支持了kotlin协程的suspend挂起函数),

之前了解Retrofit的小伙伴应该知道,Retrofit是有同步和异步两种调用方式的。

void enqueue(Callback<T> callback);

上面这就是异步调用方式,传入一个Callback,这也是我们最最最常用到的方式。

Response<T> execute() throws IOException;

上面这种是同步调用方法,会阻塞线程,返回的直接就是网络数据Response,很少使用。

后来我就在思考,能不能结合kotlin的协程,抛弃Callback,直接使用Retrofit的同步方法,把异步当同步写,代码顺序书写,逻辑清晰,效率高,同步的写法就更加方便对象的管理。

说干就干。

首先写一个协程的扩展方法:

val api = ……
fun <ResultType> CoroutineScope.retrofit() {
    this.launch(Dispatchers.Main) {
        val work = async(Dispatchers.IO) {
            try {
                api.execute() // 调用同步方法
            } catch (e: ConnectException) {
                e.logE()
                println("网络连接出错")
                null
            } catch (e: IOException) {
                println("未知网络错误")
                null
            }
        }
        work.invokeOnCompletion { _ ->
            // 协程关闭时,取消任务
            if (work.isCancelled) {
                api.cancel() // 调用 Retrofit 的 cancel 方法关闭网络
            }
        }
        val response = work.await() // 等待io任务执行完毕返回数据后,再继续后面的代码

        response?.let {

            if (response.isSuccessful) {
                println(response.body()) //网络请求成功,获取到的数据
            } else {
                // 处理 HTTP code
                when (response.code()) {
                    401 -> {
                    }
                    500 -> {
                        println("内部服务器错误")
                    }
                }
                println(response.errorBody()) //网络请求失败,获取到的数据
            }

        }
    }
}

上面就是核心代码,主要的意思都写了注释。整个工作流程是出于ui协程中,所以可以随意操作UI控件,接着在io线程中去同步调用网络请求,并且等待io线程的执行完毕,接着再拿到结果进行处理,整个流程都是基于同步代码的书写方式,一步一个流程,没有回掉而导致的代码割裂感。那么继续,我们想办法把获取的数据返回出去。

这里我们采用DSL方法,首先自定义一个类:

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

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

    var showFailedMsg = false

    internal fun clean() {
        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
    }

}

此类对外暴露了三个方法:onSuccess,onComplete,onFailed,用于分类返回数据。

接着,我们对我们的核心代码进行改造,将方法进行传递:

fun <ResultType> CoroutineScope.retrofit(
    dsl: RetrofitCoroutineDsl<ResultType>.() -> Unit //传递方法,需要哪个,传递哪个
) {
    this.launch(Dispatchers.Main) {
        val retrofitCoroutine = RetrofitCoroutineDsl<ResultType>()
        retrofitCoroutine.dsl()
        retrofitCoroutine.api?.let { it ->
            val work = async(Dispatchers.IO) { // io线程执行
                try {
                    it.execute()
                } catch (e: ConnectException) {
                    e.logE()
                    retrofitCoroutine.onFailed?.invoke("网络连接出错", -100)
                    null
                } catch (e: IOException) {
                    retrofitCoroutine.onFailed?.invoke("未知网络错误", -1)
                    null
                }
            }
            work.invokeOnCompletion { _ ->
                // 协程关闭时,取消任务
                if (work.isCancelled) {
                    it.cancel()
                    retrofitCoroutine.clean()
                }
            }
            val response = work.await()
            retrofitCoroutine.onComplete?.invoke()
            response?.let {
                    if (response.isSuccessful) {
                        retrofitCoroutine.onSuccess?.invoke(response.body())
                    } else {
                        // 处理 HTTP code
                        when (response.code()) {
                            401 -> {
                            }
                            500 -> {
                            }
                        }
                        retrofitCoroutine.onFailed?.invoke(response.errorBody(), response.code())
                    }
            }
        }
    }
}

这里使用DSL传递方法,可以更具需要传递的,例如只需要onSuccess,那就只传递这一个方法,不必三个都传递,按需使用。

使用方式:

首先需要按照kotlin的官方文档来改造下activity:

abstract class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job // 定义job

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

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

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // 关闭页面后,结束所有协程任务
    }
}

Activity实现CoroutineScope接口,就能直接根据当前的context获取协程使用。

接下来就是真正的使用,在任意位置即可调用此扩展方法:

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

    onComplete {
    }

    onSuccess { str ->
    }

    onFailed { error, code ->
    }
}

在有的时候,我们只需要处理onSuccess的情况,并不关心其他两个。那么直接写:

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

    onSuccess { str ->
    }
}

需要哪个写哪个,代码非常整洁。可以看出,我们不需要单独再对网络请求进行生命周期的绑定,在页面被销毁的时候,job也就被关闭了,当协程被关闭后,会执行调用 Retrofit 的 cancel方法关闭网络。

5. 小节

协程的开销是小于Thread多线程的,响应速度很快,非常适合轻量化的工作流程。对于协程的使用,还有带我更深入的思考和学习。协程并不是Thread的替代品,还是多异步任务多一个补充,我们不能按照惯性思维去理解协程,而是要多从其本身特性入手,开发出它更安逸的使用方式。

而且随着Retrofit 2.6.0的发布,自带了新的协程方案,如下:

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

增加了suspend挂起函数的支持,可见协程的应用会越来越受欢迎。上面所说的所有网络处理方法,不论是Rx还是LiveData,都是很好的封装方式,技术没有好坏之分。我的协程封装方式,也许也不是最好的,但是我们不能缺乏思考、探索、实践三要素,去想去做。

最好的答案,永远都是自己给出的。

关于Kotlin的学习资料

需要的私信我【学习】我分享给你!

相关推荐

ubuntu单机安装open-falcon极度详细操作

备注:以下操作均由本人实际操作并得到验证,喜欢的同学可尝试操作安装。步骤一1.1环境准备(使用系统:ubuntu18.04)1.1.1安装redisubuntu下安装(参考借鉴:https://...

Linux搭建promtail、loki、grafana轻量日志监控系统

一:简介日志监控告警系统,较为主流的是ELK(Elasticsearch、Logstash和Kibana核心套件构成),虽然优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,...

一文搞懂,WAF阻止恶意攻击的8种方法

WAF(Web应用程序防火墙)是应用程序和互联网流量之间的第一道防线,它监视和过滤Internet流量以阻止不良流量和恶意请求,WAF是确保Web服务的可用性和完整性的重要安全解决方案。它...

14配置appvolume(ios14.6配置文件)

使用AppVolumes应用程序功能,您可以管理应用程序的整个生命周期,包括打包、更新和停用应用程序。您还可以自定义应用程序分配,以向最终用户提供应用程序的特定版本14.1安装appvolume...

目前流行的缺陷管理工具(缺陷管理方式存在的优缺点)

摘自:https://blog.csdn.net/jasonteststudy/article/details/7090127?utm_medium=distribute.pc_relevant.no...

开源数字货币交易所开发学习笔记(2)——SpringCloud

前言码云(Gitee)上开源数字货币交易所源码CoinExchange的整体架构用了SpringCloud,对于经验丰富的Java程序员来说,可能很简单,但是对于我这种入门级程序员,还是有学习的必要的...

开发JAX-RPC Web Services for WebSphere(下)

在开发JAX-RPCWebServicesforWebSphere(上)一文中,小编为大家介绍了如何创建一个Web服务项目、如何创建一个服务类和Web服务,以及部署项目等内容。接下来小编将为大...

CXF学习笔记1(cxf client)

webservice是发布服务的简单并实用的一种技术了,个人学习了CXF这个框架,也比较简单,发布了一些笔记,希望对笔友收藏并有些作用哦1.什么是webServicewebService让一个程序可...

分布式RPC最全详解(图文全面总结)

分布式通信RPC是非常重要的分布式系统组件,大厂经常考察的Dubbo等RPC框架,下面我就全面来详解分布式通信RPC@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》...

Oracle WebLogic远程命令执行0day漏洞(CVE-2019-2725补丁绕过)预警

概述近日,奇安信天眼与安服团队通过数据监控发现,野外出现OracleWebLogic远程命令执行漏洞最新利用代码,此攻击利用绕过了厂商今年4月底所发布的最新安全补丁(CVE-2019-2725)。由...

Spring IoC Container 原理解析(spring中ioc三种实现原理)

IoC、DI基础概念关于IoC和DI大家都不陌生,我们直接上martinfowler的原文,里面已经有DI的例子和spring的使用示例《InversionofControlContainer...

Arthas线上服务器问题排查(arthas部署)

1Arthas(阿尔萨斯)能为你做什么?这个类从哪个jar包加载的?为什么会报各种类相关的Exception?我改的代码为什么没有执行到?难道是我没commit?分支搞错了?遇到问题无法在...

工具篇之IDEA功能插件HTTP_CLENT(idea2021插件)

工具描述:Java开发人员通用的开发者工具IDEA集成了HTTPClient功能,之后可以无需单独安装使用PostMan用来模拟http请求。创建方式:1)简易模式Tools->HTTPCl...

RPC、Web Service等几种远程监控通信方式对比

几种远程监控通信方式的介绍一.RPCRPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.meth...

《github精选系列》——SpringBoot 全家桶

1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...

取消回复欢迎 发表评论: