忘了RxJava:Kotlin Coroutines才是你需要的

原文地址:https://proandroiddev.com/forget-rxjava-kotlin-coroutines-are-all-you-need-part-1-2-4f62ecc4f99b
使用RxJava创建GithubApi的网络层的接口如下:

interface ApiClientRx {

    fun login(auth: Authorization) : Single<GithubUser>
    fun getRepositories(reposUrl: String, auth: Authorization) : Single<List<GithubRepository>>
    fun searchRepositories(query: String) : Single<List<GithubRepository>>
  
}

虽然Rxjava是一个功能强大的库,但它不能用作管理异步任务的工具,他是一个事件处理库。
接口使用方法如下所示:

private fun attemptLoginRx(){
  val login = email.text.toString()
  val pass = password.test.toString()
  
  val auth = BasicAuthorization(login, pass)
  val apiClient = ApiClientRx.ApiClientRxImpl()
  showProgressVisible(true)
  compositeDisposable.add(apiClient.login(auth)
    .flatMap {
        user -> apiClient.getRepositories(user.reposUrl, auth)
    }
    .map { list -> list.map { it.fullName } }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .doAfterTerminate { showProgressVisible(false) }
    .subscribe(
            { list -> showRepositories(this@LoginActivity, list) },
            { error -> Log.e("TAG", "Failed to show repos", error) }
    ))
}

这段代码有几个隐含的缺陷:

性能开销

这里的每一行都生成一个内部对象来完成这项工作,对于这种特殊情况,他生成了19个对象,如果更复杂的例子,那么将会生成更多的对象:


image.png

不可读的堆栈跟踪

如果你的代码中发生了一个错误,那么如下的堆栈跟踪相当不好理解:

at com.epam.talks.github.model.ApiClientRx$ApiClientRxImpl$login$1.call(ApiClientRx.kt:16)
at io.reactivex.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:44)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.internal.operators.single.SingleFlatMap.subscribeActual(SingleFlatMap.java:36)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.internal.operators.single.SingleMap.subscribeActual(SingleMap.java:34)
at io.reactivex.Single.subscribe(Single.java:3096)
at io.reactivex.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89)
at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:463)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)

整个堆栈跟踪中只有一行来自你的代码。

学习的复杂性

还记得你花了多少时间用来理解map()和flatmap()的不同之处吗。更不用说还有其他更多的操作符,这些时间是每个新的开发人员进入响应式变成世界都需要花费的。

Kotlin Coroutines能否让我们的生活更好?

public actual interface Deferrd<out T>: Job {
  public suspend fun await() : T
}

interface Job:CoroutineContext.Element {
  public val isActive: Boolean
  public val isCompleted: Boolean
  public val isCancelled: Boolean
  public fun getCancellationException: CancellationException
  public fun start(): Boolean
}

接下来重构代码:

interface ApiClient {
  fun login(auth: Authorization) : Deferred<GithubUser>
  fun getRepositories(reposUrl: String, auth: Authorization): Deffered<List<GithubRepository>>
}
override fun login(auth: Authorization) : Deferred<GithubUser?> = async {
    val response = get("https://api.github.com/user", auth = auth)
    if (response.statusCode != 200) {
        throw RuntimeException("Incorrect login or password")
    }

    val jsonObject = response.jsonObject
    with (jsonObject) {
        return@async GithubUser(getString("login"), getInt("id"),
                getString("repos_url"), getString("name"))
    }
}

acync和launch构建器使用CommonPool调度程序。

job = launch(UI){
  showProgressVisible(true);
  val auth = BasicAuthorization(login,pass)
  try{
    val userInfo = login(auth).await()
    val repoUrl = userInfo.reposUrl
    val repos = getRepositories(repoUrl, auth).await()
    val pullRequests = getPullRequests(repos[0], auth).await()
    showRepositories(this, repos.map { it -> it.fullName })
  } catch (e: RuntimeException) {
    Toast.makeText(this, e.message, LENGTH_LONG).show()
  } finally {
    showProgressVisible(false)
  }
}

代码变得更加的清晰和直观,看起来就像没有任何异步发生,在RxJava中我们还需要将subscription添加到compositeDisposable中,以便将它放在onStop中。

性能

对象数量下降到了11


image.png

不可读的堆栈

堆栈信息依旧不可读但是这个问题正在被解决.

可读性

代码更易于读写,因为是用同步的方法编写的。

如何重构测试代码?

使用RxJava的测试代码是如下的样子:

@Test
    fun login() {
        val apiClientImpl = ApiClientRx.ApiClientRxImpl()
        val genericResponse = mockLoginResponse()

        staticMockk("khttp.KHttp").use {
            every { get("https://api.github.com/user", auth = any()) } returns genericResponse

            val githubUser = apiClientImpl.login(BasicAuthorization("login", "pass"))

            githubUser.subscribe { githubUser ->
                Assert.assertNotNull(githubUser)
                Assert.assertEquals("name", githubUser.name)
                Assert.assertEquals("url", githubUser.reposUrl)
            }

        }
    }

使用kotin Coroutine 之后的测试代码如下:

@Test
    fun login() {
        val apiClientImpl = ApiClient.ApiClientImpl()
        val genericResponse = mockLoginResponse()

        staticMockk("khttp.KHttp").use {
            every { get("https://api.github.com/user", auth = any()) } returns genericResponse

            runBlocking {
                val githubUser = apiClientImpl.login(BasicAuthorization("login", "pass")).await()

                assertNotNull(githubUser)
                assertEquals("name", githubUser.name)
                assertEquals("url", githubUser.repos_url)
            }
        }
    }

还有更进一步的改进吗?

我们使用suspend修饰符替换Deferred对象

interface SuspendingApiClient {

    suspend fun login(auth: Authorization) : GithubUser
    suspend fun getRepositories(reposUrl: String, auth: Authorization) : List<GithubRepository>
    suspend fun searchRepositories(searchQuery: String) : List<GithubRepository>

}

接下来看一下客户端代码和测试代码的改变:

private fun attemptLoginSuspending() {
        val login = email.text.toString()
        val pass = password.text.toString()
        val apiClient = SuspendingApiClient.SuspendingApiClientImpl()
        job = launch(UI) {
            showProgressVisible(true)
            val auth = BasicAuthorization(login, pass)
            try {
                val userInfo = async(parent = job) { apiClient.login(auth) }.await()
                val repoUrl = userInfo.repos_url
                val list = async(parent = job) { apiClient.getRepositories(reposUrl, auth) }.await()
                showRepositories(this, list.map { it -> it.fullName })
            } catch (e: RuntimeException) {
                Toast.makeText(this, e.message, LENGTH_LONG).show()
            } finally {
                showProgressVisible(false)
            }
        }
    }

跟之前的对比只加了async(parent=job){}

为了在onStop中取消job时取消此协同程序,需要传递父对象。

@Test
fun testLogin() = runBlocking {
    val apiClient = mockk<SuspendingApiClient.SuspendingApiClientImpl>()
    val githubUser = GithubUser("login", 1, "url", "name")
    val repositories = GithubRepository(1, "repos_name", "full_repos_name")

    coEvery { apiClient.login(any()) } returns githubUser
    coEvery { apiClient.getRepositories(any(), any()) } returns repositories.asList()

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,963评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,191评论 9 118
  • 今天有小伙伴向我咨询:有朋友要我分享课程给她,我担心,如果她们都去听老师的课,不愿意跟我咨询了,我就担心自己没有价...
    蝉衣cy阅读 196评论 0 0
  • 梁爽 理性天蝎女 治拎不清、玻璃心、拧巴症 点燃你自律的心 新书《你来人间一趟,你要发光发亮》正在销魂出售中 前几...
    哪梁爽哪呆着阅读 475评论 0 6