(最初在
Medium上发布)
Kotlin协程不仅仅只是轻量级线程-它们是一种新的范例,可以帮助开发人员以
结构化和惯用的方式处理并发。
开发Android应用程序时,应考虑许多不同的事情:从UI线程中删除长时间运行的操作,处理生命周期事件,取消订阅,切换回UI线程以更新用户界面。 在过去的两年中,RxJava成为解决这一系列问题的最常用框架之一。 在本文中,我将指导您完成从RxJava到协程的端到端功能迁移。
特色
我们要转换为协程的功能非常简单:当用户提交国家/地区时,我们会进行API调用,以检查该国家/地区是否可以通过
Companies House等提供程序进行业务详细信息查询。 如果呼叫成功,我们将显示响应,否则显示错误消息。
迁徙

我们将以自下而上的方法迁移代码,从Retrofit服务开始,再移至存储库层,然后移至Interactor层,最后移至ViewModel。
当前返回Single的函数应成为挂起函数,而返回Observable的函数应返回Flow。 在此特定示例中,我们将不对Flows做任何事情。
翻新服务
让我们直接进入代码,并将BusinessLookupService中的businessLookupEligibility方法重构为协程。 这就是现在的样子。
interface BusinessLookupService { @GET("v1/eligibility") fun businessLookupEligibility( @Query("countryCode") countryCode: String ): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>> }
重构步骤:
- 从2.6.0版开始,Retrofit支持suspend修饰符。 让我们将businessLookupEligibility方法变成一个暂停函数。
- 从返回类型中删除Single包装器。
interface BusinessLookupService { @GET("v1/eligibility") suspend fun businessLookupEligibility( @Query("countryCode") countryCode: String ): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse> }
NetworkResponse是一个密封的类,表示BusinessLookupEligibilityResponse或ErrorResponse。 NetworkResponse是在自定义Retrofit呼叫适配器中构造的。 这样,我们将数据流限制为仅两种可能的情况-成功或错误,因此BusinessLookupService的使用者无需担心异常处理。
资料库
让我们继续前进,看看我们在BusinessLookupRepository中拥有什么。 在businessLookupEligibility方法主体中,我们称为businessLookupService.businessLookupEligibility(我们刚刚重构的对象),并使用RxJava的map运算符将NetworkResponse转换为Result并将响应模型映射到域模型。 Result是另一个密封的类,它表示Result.Success,并且如果网络调用成功,则包含BusinessLookupEligibility对象。 如果网络调用中出现错误,反序列化异常或其他错误,我们将使用有意义的错误消息(ErrorMessage是String的类型
别名 )构造Result.Failure。
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()) } }
重构步骤:
- businessLookupEligibility变为挂起函数。
- 从返回类型中删除Single包装器。
- 存储库中的方法通常执行长时间运行的任务,例如网络调用或数据库查询。 存储库负责指定应在哪个线程上执行此工作。 通过subscriptionOn(schedulerProvider.io()),我们告诉RxJava应该在io线程上完成工作。 协程如何实现相同目的? 我们将withwithContext与特定的调度程序一起使用,以将块的执行转移到不同的线程上,并在执行完成后转移回原始的调度程序。 最好使用withContext确保函数是主安全的。 BusinessLookupRepository的使用者不应考虑应该使用哪个线程来执行businessLookupEligibility方法,应安全地从主线程调用它。
- 我们不再需要地图运算符,因为我们可以在暂停函数的主体中使用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) ) } } }
牵连器
在此特定示例中,BusinessLookupEligibilityInteractor不包含任何其他逻辑,并用作BusinessLookupRepository的代理。 我们使用调用
运算符重载,以便可以将交互器作为一个函数来调用。
class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> = businessLookupRepository.businessLookupEligibility(countryCode) }
重构步骤:
- 操作员有趣的调用变为暂停操作员有趣的调用。
- 从返回类型中删除Single包装器。
class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = businessLookupRepository.businessLookupEligibility(countryCode) }
视图模型
在BusinessProfileViewModel中,我们调用返回Single的BusinessLookupEligibilityInteractor。 我们订阅流并通过指定UI调度程序在UI线程上对其进行观察。 如果成功,我们会将域模型中的值分配给businessViewState LiveData。 如果出现故障,我们将分配一条错误消息。
我们将每个订阅添加到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(); } }
重构步骤:
- 在本文的开头,我提到了协程的主要优点之一-结构化并发。 这就是它发挥作用的地方。 每个协程都有一个范围。 示波器通过其工作可以控制协程。 如果取消作业,则相应范围内的所有协程也将被取消。 您可以自由创建自己的范围,但是在这种情况下,我们将利用ViewModel生命周期感知的viewModelScope。 我们将使用viewModelScope.launch在viewModelScope中启动一个新的协程。 协程将在主线程中启动,因为viewModelScope具有默认的分派器-Dispatchers.Main。 一个协程在Dispatchers上启动,Main在挂起时不会阻塞主线程。 当我们刚刚启动了协程时,我们可以调用businessLookupEligibilityInteractor挂起运算符并获取结果。 businessLookupEligibilityInteractor调用BusinessLookupRepository.businessLookupEligibility,这会将执行转移到Dispatchers.IO,然后又转移到Dispatchers.Main。 正如我们在UI线程中一样,我们可以通过分配一个值来更新businessViewState LiveData。
- 由于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 } } } }
重要要点
读取和理解用协程编写的代码非常容易,但是这是一种范式转换,需要一些努力来学习如何使用协程编写代码。
在本文中,我没有介绍测试。 我在使用Mockito测试协程时遇到问题,因此使用了
ockk库。
我发现用Rx Java编写的所有内容都非常容易使用协程,
流和
通道来实现。 协程的优点之一是它们是Kotlin语言的功能,并且与语言一起发展。