使用kotlin协程来代替RxJava

我们的项目可能在以下场景中用到了RxJava:

  • 网络请求(Retrofit)
  • 并发任务
  • RxView
  • 权限请求(RxPermission)
  • startActivityForResult(RxImagePicker、RxFilePicker)

我们接下来会讲如何使用kotlin的协程来代替RxJava

网络请求

我们以玩安卓首页banner的接口为例
地址
http service的写法如下,注意返回的类型是Observable

 @GET("banner/json")
 fun banner(): Observable<BaseJsonListResponse<Banner>>

创建service的时候指定AdapterFactory为RxJava2CallAdapterFactory

 val rxJavaService: RxJavaService by lazy {
        Retrofit.Builder()
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl(baseUrl)
            .build().create(RxJavaService::class.java)
    }

调用方法如下

val disposable = Instance.rxJavaService
                .banner()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                           showResult(it, "使用RxJava获取banner")
                }
            compositeDisposable.add(disposable)

而当我们使用协程的时候,http service的写法就变成了下面这种

@GET("banner/json")
    suspend fun banner(): BaseJsonListResponse<Banner>

方法加上suspend关键字
创建service的时候不需要再指定AdapterFactory

 val coroutineService: CoroutineService by lazy {
        Retrofit.Builder()
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(baseUrl)
            .build().create(CoroutineService::class.java)
    }

调用方法如下

lifecycleScope.launch {
                val result = Instance.coroutineService.banner()
                showResult(result, "使用协程获取banner")
            }

看着比RxJava更加的直观,毕竟协程就是让你以写同步代码的方式写异步代码

并发任务

在实际的业务场景中,我们可能会遇到要同时并发进行耗时操作的情况,比如上传图片的时候并发压缩所有照片,而不是一个一个顺序压缩
比如我们压缩图片的方法是compressBitmap

private fun compressBitmap(value: Long): Long {
        log("在 ${Thread.currentThread().name} 线程中处理")
        Thread.sleep(value)
        return value
    }

我们使用Thread.sleep来模拟耗时操作
现在我们来并发对一个图片数组进行压缩

 val bitmapList = listOf<Long>(3000, 3000, 4000)

 binding.taskWithRxjava.setOnClickListener {
            val start = System.currentTimeMillis()

            Observable.zip(bitmapList.map {
                Observable
                    .just(it)
                    .observeOn(Schedulers.io())
                    .map { value ->
                        compressBitmap(value)
                    }
            }) {

                it
            }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {

                    AlertDialog.Builder(this)
                        .setTitle("获取到结果 任务耗时 ${System.currentTimeMillis() - start}毫秒")
                        .setMessage(it.toString())
                        .setPositiveButton("确定", null)
                        .show()
                }.addTo(compositeDisposable)
        }

我们同时压缩3张图片,大概4秒后,我们可以看到一个提示成功的弹窗,再看log日志,可以看出三个压缩任务在不同的线程执行

2021-08-17 09:57:30.463 11705-11751/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-3 线程中处理
2021-08-17 09:57:30.463 11705-11749/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-1 线程中处理
2021-08-17 09:57:30.463 11705-11750/com.ke.rxjava_coroutine D/TAG: 在 RxCachedThreadScheduler-2 线程中处理

现在我们使用协程来进行并发耗时操作
代码如下

 lifecycleScope.launch {
                val start = System.currentTimeMillis()
                val result = bitmapList.map {
                    GlobalScope.async {
                        compress(it)
                    }
                }
                    .map {
                        it.await()
                    }

                AlertDialog.Builder(this@MainActivity)
                    .setTitle("协程并发 获取到结果 任务耗时 ${System.currentTimeMillis() - start}毫秒")
                    .setMessage(result.toString())
                    .setPositiveButton("确定", null)
                    .show()
            }

同样,大约4秒后,我们看到弹窗提示处理完成,同时通过日志也能看出三个任务在不同线程中进行

2021-08-17 10:09:56.081 13978-14300/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-1 线程中处理
2021-08-17 10:09:56.081 13978-14301/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-2 线程中处理
2021-08-17 10:09:56.082 13978-14302/com.ke.rxjava_coroutine D/TAG: 在 DefaultDispatcher-worker-3 线程中处理

RxView

RxView把View的事件(点击,选中状态改变,文字改变)都变成了Observable,我们可以以处理流的形式处理View的事件,这里主要讨论两个场景

  • 防止重复点击
    使用RxJava处理方式如下
binding.clickWithRxjava.clicks()
            .throttleFirst(5, java.util.concurrent.TimeUnit.SECONDS)
            .subscribe {
                log("点击了按钮")
            }

加了throttleFirst后,订阅到的点击事件在不停点击按钮后每5秒只会触发一次
使用flow可以达到类似的效果

@ExperimentalCoroutinesApi
fun View.clickEventFlow(): Flow<Unit> {

    var lastClickTime = System.currentTimeMillis()

    return callbackFlow {
        setOnClickListener {
            if (System.currentTimeMillis() - lastClickTime > 5000) {
                lastClickTime = System.currentTimeMillis()
                offer(Unit)
            }
        }
        awaitClose {
            setOnClickListener(null)
        }
    }
}
  • 优化搜索
    大致意思是如果用户输入的是123456789,我们希望只发出一次123456789网络请求,而不是输入一个1发一次网络请求,输入一个2发出12的网络请求
    使用RxView处理方式如下
 binding.etContent.textChanges().debounce(1, TimeUnit.SECONDS)
            .subscribe {
                log("输入框信息发生变化 来自RxJava $it")
            }

在停止输入一秒后才发起网络请求,避免不必要的网络请求
我们也可以使用Flow来实现这个功能。首先把TextView的文字变化转换成Flow


@ExperimentalCoroutinesApi
fun TextView.textChangeFlow(): Flow<CharSequence> {

    return callbackFlow {

        val watcher = object : TextWatcher {
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {

            }

            override fun onTextChanged(content: CharSequence?, p1: Int, p2: Int, p3: Int) {
                if (content != null) {
                    offer(content)
                }
            }

            override fun afterTextChanged(p0: Editable?) {
            }

        }

        addTextChangedListener(watcher)
        awaitClose {
            removeTextChangedListener(watcher)
        }

    }
}

然后同样需要加上debounce

 lifecycleScope.launch {
            binding.etContent.textChangeFlow().debounce(1000)
                .collect {
                    log("输入框信息发生变化 来自Flow $it")

                }
        }

权限请求

不使用任何库请求权限的方式如下:

ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 422
            )

onRequestPermissionsResult回调方法里判断用户是否授权

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if(requestCode == 422){
            
        }
    }

使用RxPermissions请求就看起来方便很多,代码如下

 RxPermissions(this)
                .request(Manifest.permission.ACCESS_FINE_LOCATION)
                .subscribe {
                    log("使用RxJava请求权限结果 $it")
                }

RxPermissions的原理是创建一个新的Fragment去请求权限,我们也可以利用这点建立一个不可见的Fragment并用它去请求权限和处理回调。
Github上已经有一个现成的框架,地址
不用协程,仅用Kotlin的方法参数就可以实现。

startActivityForResult(RxImagePicker、RxFilePicker)

和上面说的一样,创建一个不可见的fragment来启动activity和处理回调

结尾

现在,可以放弃RxJava了吗

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

推荐阅读更多精彩内容