Сопрограммы Kotlin — это гораздо больше, чем просто легкие потоки — это новая парадигма, которая помогает разработчикам справляться с параллелизмом структурированным и идиоматическим образом.
При разработке приложения для Android следует учитывать множество разных вещей: удаление длительных операций из потока пользовательского интерфейса, обработка событий жизненного цикла, отмена подписок, переключение обратно в поток пользовательского интерфейса для обновления пользовательского интерфейса.
Вам также может понравиться:
Параллельное программирование в Kotlin: сопрограммы
За последние пару лет RxJava стала одной из наиболее часто используемых сред для решения этого набора проблем. В этой статье я расскажу вам о сквозной миграции функций из RxJava в сопрограммы.
Характерная черта
Функция, которую мы собираемся преобразовать в сопрограммы, довольно проста: когда пользователь отправляет страну, мы выполняем вызов API, чтобы проверить, имеет ли страна право на поиск сведений о бизнесе через такого поставщика, как Companies House . Если звонок был успешным, мы показываем ответ, если нет — сообщение об ошибке.
миграция
Мы собираемся перенести наш код в подход «снизу вверх», начиная со службы Retrofit, переходя на уровень репозитория, затем на уровень Interactor и, наконец, в модель представления.
Функции, которые в настоящее время возвращаются, Single
должны стать функциями приостановки, а функции, которые возвращаются, Observable
должны возвращаться Flow
. В этом конкретном примере мы не собираемся ничего делать с потоками.
Модернизация Сервис
Давайте сразу перейдем к коду и проведем рефакторинг businessLookupEligibility
метода в BusinessLookupService
сопрограммы. Вот как это выглядит сейчас.
Котлин
1
interface BusinessLookupService {
2
@GET("v1/eligibility")
3
fun businessLookupEligibility(
4
@Query("countryCode") countryCode: String
5
): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>>
6
}
Рефакторинг этапов:
- Начиная с версии 2.6.0 , Retrofit поддерживает
suspend
модификатор. Давайте превратимbusinessLookupEligibility
метод в функцию приостановки. - Удалите
Single
оболочку из возвращаемого типа.
Котлин
xxxxxxxxxx
1
interface BusinessLookupService {
2
@GET("v1/eligibility")
3
suspend fun businessLookupEligibility(
4
@Query("countryCode") countryCode: String
5
): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>
6
}
NetworkResponse
является запечатанным классом, который представляет BusinessLookupEligibilityResponse
или ErrorResponse
. NetworkResponse
построен в настраиваемом адаптере вызовов Retrofit Таким образом, мы ограничиваем поток данных только двумя возможными случаями — успехом или ошибкой, поэтому пользователям BusinessLookupService
не нужно беспокоиться об обработке исключений.
вместилище
Давайте двигаться дальше и посмотрим, что у нас есть BusinessLookupRepository
. В businessLookupEligibility
теле метода мы вызываем businessLookupService.businessLookupEligibility
(тот, который мы только что реорганизовали) и используем map
оператор RxJava для преобразования NetworkResponse
в Result
и сопоставляем модель ответа с моделью предметной области. Result
является другим запечатанным классом, который представляет Result.Success
и содержит BusinessLookupEligibility
объект в случае, если сетевой вызов был успешным. Если в сетевом вызове произошла ошибка, исключение десериализации или что-то еще пошло не так, мы создаем Result.Failure
значимое сообщение об ошибке ( ErrorMessage
это typealias для String).
Котлин
xxxxxxxxxx
1
class BusinessLookupRepository @Inject constructor(
2
private val businessLookupService: BusinessLookupService,
3
private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper,
4
private val responseToString: Mapper,
5
private val schedulerProvider: SchedulerProvider
6
) {
7
fun businessLookupEligibility(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> {
8
return businessLookupService.businessLookupEligibility(countryCode)
9
.map { response ->
10
return@map when (response) {
11
is NetworkResponse.Success -> {
12
val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body)
13
Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility)
14
}
15
is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>(
16
responseToString.transform(response)
17
)
18
}
19
}.subscribeOn(schedulerProvider.io())
20
}
21
}
Рефакторинг этапов:
businessLookupEligibility
становитсяsuspend
функцией.- Удалите
Single
оболочку из возвращаемого типа. - Методы в репозитории обычно выполняют длительные задачи, такие как сетевые вызовы или запросы к БД. Репозиторий несет ответственность за указание того, в каком потоке должна выполняться эта работа. К
subscribeOn(schedulerProvider.io()),
мы говорим RxJava , что работа должна быть сделана наio
волоске. Как этого можно достичь с помощью сопрограмм? Мы собираемся использоватьwithContext
с конкретным диспетчером для переноса выполнения блока в другой поток и обратно к исходному диспетчеру, когда выполнение завершится. Рекомендуется убедиться, что функция безопасна при использованииwithContext
. ПотребителиBusinessLookupRepository
не должны думать о том, какой поток они должны использовать для выполненияbusinessLookupEligibility
метода, его можно безопасно вызывать из основного потока. - Нам больше не нужен
map
оператор, так как мы можем использовать результатbusinessLookupService.businessLookupEligibility
в телеsuspend
функции.
Котлин
xxxxxxxxxx
1
class BusinessLookupRepository @Inject constructor(
2
private val businessLookupService: BusinessLookupService,
3
private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper,
4
private val responseToString: Mapper,
5
private val dispatcherProvider: DispatcherProvider
6
) {
7
suspend fun businessLookupEligibility(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> =
8
withContext(dispatcherProvider.io) {
9
when (val response = businessLookupService.businessLookupEligibility(countryCode)) {
10
is NetworkResponse.Success -> {
11
val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body)
12
Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility)
13
}
14
is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>(
15
responseToString.transform(response)
16
)
17
}
18
}
19
}
Interactor
В этом конкретном примере BusinessLookupEligibilityInteractor
не содержит никакой дополнительной логики и служит прокси для BusinessLookupRepository
. Мы используем перегрузку оператора вызова, чтобы интерактор мог быть вызван как функция.
Котлин
xxxxxxxxxx
1
class BusinessLookupEligibilityInteractor @Inject constructor(
2
private val businessLookupRepository: BusinessLookupRepository
3
) {
4
operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> =
5
businessLookupRepository.businessLookupEligibility(countryCode)
6
}
Рефакторинг этапов:
operator fun invoke
становитсяsuspend operator fun invoke
.- Удалите
Single
оболочку из возвращаемого типа.
Котлин
xxxxxxxxxx
1
class BusinessLookupEligibilityInteractor @Inject constructor(
2
private val businessLookupRepository: BusinessLookupRepository
3
) {
4
suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> =
5
businessLookupRepository.businessLookupEligibility(countryCode)
6
}
ViewModel
В BusinessProfileViewModel
мы называем , BusinessLookupEligibilityInteractor
что возвращается Single
. Мы подписываемся на поток и наблюдаем за ним в потоке пользовательского интерфейса, указав планировщик пользовательского интерфейса. В случае, если Success
мы присваиваем значение из модели домена для businessViewState
LiveData. В случае Failure
мы назначаем сообщение об ошибке.
Мы добавляем каждую подписку в a CompositeDisposable
и удаляем их в onCleared()
методе ViewModel
жизненного цикла.
Котлин
xxxxxxxxxx
1
class BusinessProfileViewModel @Inject constructor(
2
private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor,
3
private val schedulerProvider: SchedulerProvider
4
) : ViewModel() {
5
6
private val disposables = CompositeDisposable()
7
internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...")
8
fun onCountrySubmit(country: Country) {
10
disposables.add(businessLookupEligibilityInteractor(country.countryCode)
11
.observeOn(schedulerProvider.ui())
12
.subscribe { state ->
13
return@subscribe when (state) {
14
is Result.Success -> businessViewState.value = state.entity.provider
15
is Result.Failure -> businessViewState.value = state.failure
16
}
17
})
18
}
19
@Override
21
protected void onCleared() {
22
super.onCleared();
23
disposables.clear();
24
}
25
}
Рефакторинг этапов:
- В начале статьи я упомянул одно из главных преимуществ сопрограмм - структурированный параллелизм. И это то, где это входит в игру. У каждой сопрограммы есть область. Сфера имеет контроль над сопрограммой через свою работу. Если задание отменено, то все сопрограммы в соответствующей области также будут отменены. Вы можете создавать свои собственные области, но в этом случае мы собираемся использовать
ViewModel
жизненный цикл с учетомviewModelScope
. Мы начнем новую сопрограмму вviewModelScope
использованииviewModelScope.launch
. Сопрограмма будет запущена в главном потоке, посколькуviewModelScope
имеет диспетчер по умолчанию -Dispatchers.Main
. Запуск сопрограммыDispatchers.Main
не будет блокировать основной поток, пока он приостановлен. Поскольку мы только что запустили сопрограмму, мы можем вызватьbusinessLookupEligibilityInteractor
оператор приостановки и получить результат.businessLookupEligibilityInteractor
вызовы,BusinessLookupRepository.businessLookupEligibility
которые переносят выполнение наDispatchers.IO
и обратноDispatchers.Main
. Поскольку мы находимся в потоке пользовательского интерфейса, мы можем обновлятьbusinessViewState
LiveData, назначая значение. - Мы можем избавиться от того,
disposables
чтоviewModelScope
связано сViewModel
жизненным циклом. Любая сопрограмма, запущенная в этой области, автоматически отменяется, еслиViewModel
она очищена.
Котлин
xxxxxxxxxx
1
class BusinessProfileViewModel @Inject constructor(
2
private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor
3
) : ViewModel() {
4
internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...")
6
fun onCountrySubmit(country: Country) {
8
viewModelScope.launch {
9
when (val state = businessLookupEligibilityInteractor(country.countryCode)) {
10
is Result.Success -> businessViewState.value = state.entity.provider
11
is Result.Failure -> businessViewState.value = state.failure
12
}
13
}
14
}
15
}
Ключевые вынос
Читать и понимать код, написанный с сопрограммами, довольно легко. Тем не менее, это смена парадигмы, которая требует определенных усилий, чтобы научиться подходить к написанию кода с сопрограммами.
В этой статье я не освещал тестирование. Я использовал библиотеку mockk, так как у меня были проблемы с тестированием сопрограмм с помощью Mockito.
Все , что я написал с RxJava я нашел довольно легко реализовать с помощью сопрограммам, потоков , и каналы . Одним из преимуществ сопрограмм является то, что они являются языком Kotlin и развиваются вместе с языком.
Дальнейшее чтение
Параллельное программирование в Котлине: сопрограммы