原文地址: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个对象,如果更复杂的例子,那么将会生成更多的对象:
不可读的堆栈跟踪
如果你的代码中发生了一个错误,那么如下的堆栈跟踪相当不好理解:
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
不可读的堆栈
堆栈信息依旧不可读但是这个问题正在被解决.
可读性
代码更易于读写,因为是用同步的方法编写的。
如何重构测试代码?
使用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)
}
}