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

96
小菜鸟程序媛
2018.08.27 11:38 字数 565

原文地址: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)
        }
    }
kotlin
Web note ad 1