原文地址:https://proandroiddev.com/forget-rxjava-kotlin-coroutines-are-all-you-need-part-1-2-4f62ecc4f99b
使用RxJava創(chuàng)建GithubApi的網(wǎng)絡(luò)層的接口如下:
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是一個功能強大的庫,但它不能用作管理異步任務(wù)的工具讶踪,他是一個事件處理庫。
接口使用方法如下所示:
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) }
))
}
這段代碼有幾個隱含的缺陷:
性能開銷
這里的每一行都生成一個內(nèi)部對象來完成這項工作泛鸟,對于這種特殊情況,他生成了19個對象,如果更復(fù)雜的例子裕膀,那么將會生成更多的對象:
不可讀的堆棧跟蹤
如果你的代碼中發(fā)生了一個錯誤,那么如下的堆棧跟蹤相當(dāng)不好理解:
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)
整個堆棧跟蹤中只有一行來自你的代碼勇哗。
學(xué)習(xí)的復(fù)雜性
還記得你花了多少時間用來理解map()和flatmap()的不同之處嗎昼扛。更不用說還有其他更多的操作符,這些時間是每個新的開發(fā)人員進(jìn)入響應(yīng)式變成世界都需要花費的欲诺。
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
}
接下來重構(gòu)代碼:
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構(gòu)建器使用CommonPool調(diào)度程序渺鹦。
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)
}
}
代碼變得更加的清晰和直觀,看起來就像沒有任何異步發(fā)生蛹含,在RxJava中我們還需要將subscription添加到compositeDisposable中毅厚,以便將它放在onStop中。
性能
對象數(shù)量下降到了11
不可讀的堆棧
堆棧信息依舊不可讀但是這個問題正在被解決.
可讀性
代碼更易于讀寫浦箱,因為是用同步的方法編寫的吸耿。
如何重構(gòu)測試代碼?
使用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)
}
}
}
還有更進(jìn)一步的改進(jìn)嗎憎茂?
我們使用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時取消此協(xié)同程序,需要傳遞父對象锤岸。
@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)
}
}