端到端 点到点 主机到主机
![image](https://i-blog.csdnimg.cn/blog_migrate/f946cd87f361e8332957bff46b893bb4.png)
(originally published on Medium)
(最初在Medium上发布)
Kotlin coroutines are much more than just lightweight threads — they are a new paradigm that helps developers to deal with concurrency in a structured and idiomatic way.
Kotlin协程不仅仅只是轻量级线程,它们还是一个新的范例,可以帮助开发人员以结构化和惯用的方式处理并发。
When developing an Android app one should consider many different things: taking long-running operations off the UI thread, handling lifecycle events, cancelling subscriptions, switching back to the UI thread to update the user interface. In the last couple of years RxJava became one of the most commonly used frameworks to solve this set of problems. In this article I’m going to guide you through the end-to-end feature migration from RxJava to coroutines.
开发Android应用程序时,应考虑许多不同的事情:从UI线程中删除长时间运行的操作,处理生命周期事件,取消订阅,切换回UI线程以更新用户界面。 在过去的两年中,RxJava成为解决这一系列问题的最常用框架之一。 在本文中,我将指导您完成从RxJava到协程的端到端功能迁移。
特征 (Feature)
The feature we are going to convert to coroutines is fairly simple: when user submits a country we make an API call to check if the country is eligible for a business details lookup via a provider like Companies House. If the call was successful we show the response, if not — the error message.
我们要转换为协程的功能非常简单:当用户提交国家/地区时,我们会进行API调用,以检查该国家/地区是否可以通过Companies House等提供程序来查询公司详细信息。 如果呼叫成功,我们将显示响应(如果不是)-错误消息。
移民 (Migration)
![](https://i-blog.csdnimg.cn/blog_migrate/3cb8b8f819b40455c70adb36e0265c81.png)
We are going to migrate our code in a bottom-up approach starting with Retrofit service, moving up to a Repository layer, then to an Interactor layer and finally to a ViewModel.
我们将以自下而上的方法迁移代码,从Retrofit服务开始,再移至存储库层,然后移至Interactor层,最后移至ViewModel。
Functions that currently return Single should become suspending functions and functions that return Observable should return Flow. In this particular example we are not going to do anything with Flows.
当前返回Single的函数应成为挂起函数,而返回Observable的函数应返回Flow。 在此特定示例中,我们将不对Flows做任何事情。
翻新服务 (Retrofit Service)
Let’s jump straight into the code and refactor the businessLookupEligibility method in BusinessLookupService to coroutines. This is how it looks like now.
让我们直接进入代码,并将BusinessLookupService中的businessLookupEligibility方法重构为协程。 这就是现在的样子。
interface BusinessLookupService {
@GET("v1/eligibility")
fun businessLookupEligibility(
@Query("countryCode") countryCode: String
): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>>
}
Refactoring steps:
重构步骤:
Starting with version 2.6.0 Retrofit supports the suspend modifier. Let’s turn the businessLookupEligibility method into a suspending function.
从2.6.0版开始,Retrofit支持suspend修饰符。 让我们将businessLookupEligibility方法变成一个暂停函数。
- Remove the Single wrapper from the return type. 从返回类型中删除Single包装器。
interface BusinessLookupService {
@GET("v1/eligibility")
suspend fun businessLookupEligibility(
@Query("countryCode") countryCode: String
): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>
}
NetworkResponse is a sealed class that represents BusinessLookupEligibilityResponse or ErrorResponse. NetworkResponse is constructed in a custom Retrofit call adapter. In this way we restrict data flow to only two possible cases — success or error, so consumers of BusinessLookupService don’t need to worry about exception handling.
NetworkResponse是一个密封的类,表示BusinessLookupEligibilityResponse或ErrorResponse。 NetworkResponse是在自定义Retrofit呼叫适配器中构造的。 这样,我们将数据流限制在仅两种可能的情况下(成功或错误),因此BusinessLookupService的使用者不必担心异常处理。
资料库 (Repository)
Let’s move on and see what we have in BusinessLookupRepository. In the businessLookupEligibility method body we call businessLookupService.businessLookupEligibility (the one we have just refactored) and use RxJava’s map operator to transform NetworkResponse to a Result and map response model to domain model. Result is another sealed class that represents Result.Success and contains theBusinessLookupEligibility object in case if the network call was successful. If there was an error in the network call, deserialization exception or something else went wrong we construct Result.Failure with a meaningful error message (ErrorMessage is typealias for String).
让我们继续前进,看看我们在BusinessLookupRepository中拥有什么。 在businessLookupEligibility方法主体中,我们称为businessLookupService.businessLookupEligibility(我们刚刚重构的那个),并使用RxJava的map运算符将NetworkResponse转换为Result并将响应模型映射到域模型。 Result是另一个密封的类,它表示Result.Success,并在网络调用成功的情况下包含BusinessLookupEligibility对象。 如果网络调用中出现错误,反序列化异常或其他错误,我们将构造Result.Failure并显示有意义的错误消息(ErrorMessage是String的类型别名 )。
class BusinessLookupRepository @Inject constructor(
private val businessLookupService: BusinessLookupService,
private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper,
private val responseToString: Mapper,
private val schedulerProvider: SchedulerProvider
) {
fun businessLookupEligibility(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> {
return businessLookupService.businessLookupEligibility(countryCode)
.map { response ->
return@map when (response) {
is NetworkResponse.Success -> {
val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body)
Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility)
}
is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>(
responseToString.transform(response)
)
}
}.subscribeOn(schedulerProvider.io())
}
}
Refactoring steps:
重构步骤:
- businessLookupEligibility becomes a suspend function. businessLookupEligibility变为挂起函数。
- Remove the Single wrapper from the return type. 从返回类型中删除Single包装器。
- Methods in the repository are usually performing long-running tasks such as network calls or db queries. It is a responsibility of the repository to specify on which thread this work should be done. By subscribeOn(schedulerProvider.io()) we are telling RxJava that work should be done on the io thread. How could the same be achieved with coroutines? We are going to use withContext with a specific dispatcher to shift execution of the block to the different thread and back to the original dispatcher when the execution completes. It’s a good practice to make sure that a function is main-safe by using withContext. Consumers of BusinessLookupRepository shouldn’t think about which thread they should use to execute the businessLookupEligibility method, it should be safe to call it from the main thread. 存储库中的方法通常执行长时间运行的任务,例如网络调用或数据库查询。 存储库负责指定应在哪个线程上执行此工作。 通过subscriptionOn(schedulerProvider.io()),我们告诉RxJava应该在io线程上完成工作。 用协程怎么能达到相同目的? 我们将withwithContext与特定的调度程序一起使用,以将块的执行转移到不同的线程上,并在执行完成后移回原始的调度程序。 最好使用withContext确保函数是主安全的。 BusinessLookupRepository的使用者不应考虑应该使用哪个线程来执行businessLookupEligibility方法,应该从主线程中调用它是安全的。
- We don’t need the map operator anymore as we can use the result of businessLookupService.businessLookupEligibility in a body of a suspend function. 我们不再需要地图运算符,因为我们可以在暂停函数的主体中使用businessLookupService.businessLookupEligibility的结果。
class BusinessLookupRepository @Inject constructor(
private val businessLookupService: BusinessLookupService,
private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper,
private val responseToString: Mapper,
private val dispatcherProvider: DispatcherProvider
) {
suspend fun businessLookupEligibility(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> =
withContext(dispatcherProvider.io) {
when (val response = businessLookupService.businessLookupEligibility(countryCode)) {
is NetworkResponse.Success -> {
val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body)
Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility)
}
is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>(
responseToString.transform(response)
)
}
}
}
互动者 (Interactor)
In this specific example BusinessLookupEligibilityInteractor doesn’t contain any additional logic and serves as a proxy to BusinessLookupRepository. We use invoke operator overloading so the interactor could be invoked as a function.
在此特定示例中,BusinessLookupEligibilityInteractor不包含任何其他逻辑,并用作BusinessLookupRepository的代理。 我们使用调用运算符重载,以便可以将交互器作为一个函数来调用。
class BusinessLookupEligibilityInteractor @Inject constructor(
private val businessLookupRepository: BusinessLookupRepository
) {
operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> =
businessLookupRepository.businessLookupEligibility(countryCode)
}
Refactoring steps:
重构步骤:
- operator fun invoke becomes suspend operator fun invoke. 操作员有趣的调用变为暂停操作员有趣的调用。
- Remove the Single wrapper from the return type. 从返回类型中删除Single包装器。
class BusinessLookupEligibilityInteractor @Inject constructor(
private val businessLookupRepository: BusinessLookupRepository
) {
suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> =
businessLookupRepository.businessLookupEligibility(countryCode)
}
视图模型 (ViewModel)
In BusinessProfileViewModel we call BusinessLookupEligibilityInteractor that returns Single. We subscribe to the stream and observe it on the UI thread by specifying the UI scheduler. In case of Success we assign the value from a domain model to a businessViewState LiveData. In case of Failure we assign an error message.
在BusinessProfileViewModel中,我们将调用返回Single的BusinessLookupEligibilityInteractor。 我们订阅流,并通过指定UI调度程序在UI线程上对其进行观察。 如果成功,则将域模型中的值分配给businessViewState LiveData。 如果出现故障,我们将分配一条错误消息。
We add every subscription to a CompositeDisposable and dispose them in the onCleared() method of a ViewModel’s lifecycle.
我们将每个订阅添加到CompositeDisposable中,并将其处置在ViewModel生命周期的onCleared()方法中。
class BusinessProfileViewModel @Inject constructor(
private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor,
private val schedulerProvider: SchedulerProvider
) : ViewModel() {
private val disposables = CompositeDisposable()
internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...")
fun onCountrySubmit(country: Country) {
disposables.add(businessLookupEligibilityInteractor(country.countryCode)
.observeOn(schedulerProvider.ui())
.subscribe { state ->
return@subscribe when (state) {
is Result.Success -> businessViewState.value = state.entity.provider
is Result.Failure -> businessViewState.value = state.failure
}
})
}
@Override
protected void onCleared() {
super.onCleared();
disposables.clear();
}
}
Refactoring steps:
重构步骤:
- In the beginning of the article I’ve mentioned one of the main advantages of coroutines — structured concurrency. And this is where it comes into play. Every coroutine has a scope. The scope has control over a coroutine via its job. If a job is cancelled then all the coroutines in the corresponding scope will be cancelled as well. You are free to create your own scopes, but in this case we are going leverage theViewModel lifecycle-aware viewModelScope. We will start a new coroutine in a viewModelScope using viewModelScope.launch. The coroutine will be launched in the main thread as viewModelScope has a default dispatcher — Dispatchers.Main. A coroutine started on Dispatchers.Main will not block the main thread while suspended. As we have just launched a coroutine, we can invoke businessLookupEligibilityInteractor suspending operator and get the result. businessLookupEligibilityInteractor calls BusinessLookupRepository.businessLookupEligibility what shifts execution to Dispatchers.IO and back to Dispatchers.Main. As we are in the UI thread we can update businessViewState LiveData by assigning a value. 在本文的开头,我提到了协程的主要优点之一-结构化并发。 这就是它发挥作用的地方。 每个协程都有一个范围。 示波器通过其工作可以控制协程。 如果取消作业,则相应范围内的所有协程也将被取消。 您可以自由创建自己的范围,但是在这种情况下,我们将利用ViewModel生命周期感知的viewModelScope。 我们将使用viewModelScope.launch在viewModelScope中启动一个新的协程。 协程将在主线程中启动,因为viewModelScope具有默认的调度程序Dispatchers.Main。 在Dispatchers上启动了协程。Main在挂起时不会阻塞主线程。 刚刚启动协程时,我们可以调用businessLookupEligibilityInteractor挂起运算符并获取结果。 businessLookupEligibilityInteractor调用BusinessLookupRepository.businessLookupEligibility,这会将执行转移到Dispatchers.IO,然后又转移到Dispatchers.Main。 正如我们在UI线程中一样,我们可以通过分配一个值来更新businessViewState LiveData。
- We can get rid of disposables as viewModelScope is bound to a ViewModel lifecycle. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. 由于viewModelScope绑定到ViewModel生命周期,因此我们可以摆脱一次性用品。 如果清除ViewModel,则在此范围内启动的所有协程都会自动取消。
class BusinessProfileViewModel @Inject constructor(
private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor
) : ViewModel() {
internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...")
fun onCountrySubmit(country: Country) {
viewModelScope.launch {
when (val state = businessLookupEligibilityInteractor(country.countryCode)) {
is Result.Success -> businessViewState.value = state.entity.provider
is Result.Failure -> businessViewState.value = state.failure
}
}
}
}
重要要点 (Key takeaways)
Reading and understanding code written with coroutines is quite easy, nonetheless it’s a paradigm shift that requires some effort to learn how to approach writing code with coroutines.
阅读和理解使用协程编写的代码非常容易,但是这是一种范式转换,需要一些努力来学习如何使用协程编写代码。
In this article I didn’t cover testing. I used the mockk library as I had issues testing coroutines using Mockito.
在本文中,我没有介绍测试。 我在使用Mockito测试协程时遇到问题,因此使用了ockk库。
Everything I have written with RxJava I found quite easy to implement with coroutines, Flows and Channels. One of advantages of coroutines is that they are a Kotlin language feature and are evolving together with the language.
我发现用RxJava编写的所有内容都非常容易使用协程, 流和通道来实现。 协程的优点之一是它们是Kotlin语言的功能,并且与语言一起发展。
端到端 点到点 主机到主机