Привет, Хабр! Представляю вам перевод статьи автора Paulo Sato на тему использования Kotlin Coroutines вместо RxJava в своих Android проектах.
RxJava как базука, большинство приложений не использует и половины её огневой мощи. В статье пойдет речь о том, как заменить её корутинами (сопрограммами) Kotlin.
Я работал с RxJava в течении нескольких лет. Это определенно одна из лучших библиотек для любого Android проекта, которая и сегодня в ударе, особенно, если вы программируете на Java. Если же вы используете Kotlin, то можно сказать, что в городе новый шериф.
Большинство использует RxJava только для того, чтобы контролировать потоки и для предотвращения callback hell (если вы не знаете, что это такое, считайте себя счастливчиком и вот почему). Дело в том, что мы должны иметь ввиду, что реальная мощь RxJava — это реактивное программирование и backpressure. Если вы используете её для контроля асинхронных запросов, вы используете базуку, чтобы убить паука. Она будет делать свою работу, но это перебор.
Одним заметным недостатком RxJava является количество методов. Оно огромно и имеет тенденцию расползаться по всему коду. В Kotlin вы можете использовать корутины для реализации большей части поведения, которое вы ранее создавали, используя RxJava.
Но… что такое корутины?
Корутин — это способ обработки конкурентных задач в потоке. Поток будет работать пока не остановлен и контекст будет меняться для каждого корутина без создания нового потока.
Корутины в Kotlin всё еще являются эксперементальными, но они вошли в Kotlin 1.3, так что я написал ниже новый класс UseCase (для clean architecture), использующий их. В этом примере, вызов корутин инкапсулирован в одном файле. Таким образом, другие слои не будут зависеть от выполняемых сопрограмм, обеспечивая более разъединенную архитектуру.
/**
* (C) Copyright 2018 Paulo Vitor Sato Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.psato.devcamp.interactor.usecase
import android.util.Log
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import kotlin.coroutines.experimental.CoroutineContext
/**
* Abstract class for a Use Case (Interactor in terms of Clean Architecture).
* This interface represents a execution unit for different use cases (this means any use case
* in the application should implement this contract).
* <p>
* By convention each UseCase implementation will return the result using a coroutine
* that will execute its job in a background thread and will post the result in the UI thread.
*/
abstract class UseCase<T> {
protected var parentJob: Job = Job()
//var backgroundContext: CoroutineContext = IO
var backgroundContext: CoroutineContext = CommonPool
var foregroundContext: CoroutineContext = UI
protected abstract suspend fun executeOnBackground(): T
fun execute(onComplete: (T) -> Unit, onError: (Throwable) -> Unit) {
parentJob.cancel()
parentJob = Job()
launch(foregroundContext, parent = parentJob) {
try {
val result = withContext(backgroundContext) {
executeOnBackground()
}
onComplete.invoke(result)
} catch (e: CancellationException) {
Log.d("UseCase", "canceled by user")
} catch (e: Exception) {
onError(e)
}
}
}
protected suspend fun <X> background(context: CoroutineContext = backgroundContext, block: suspend () -> X): Deferred<X> {
return async(context, parent = parentJob) {
block.invoke()
}
}
fun unsubscribe() {
parentJob.cancel()
}
}
public class SearchShows extends UseCase {
private ShowRepository showRepository;
private ResourceRepository resourceRepository;
private String query;
@Inject
public SearchShows(ShowRepository showRepository, ResourceRepository resourceRepository) {
this.showRepository = showRepository;
this.resourceRepository = resourceRepository;
}
public void setQuery(String query) {
this.query = query;
}
@Override
protected Single<String> buildUseCaseObservable() {
return showRepository.searchShow(query).map(showInfos -> {
if (showInfos != null && !showInfos.isEmpty()
&& showInfos.get(0).getShow() != null) {
return showInfos.get(0).getShow().getTitle();
} else {
return resourceRepository.getNotFoundShow();
}
});
}
}
class SearchShows @Inject
constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) :
UseCase<String>() {
var query: String? = null
override suspend fun executeOnBackground(): String {
query?.let {
val showsInfo = showRepository.searchShow(it)
val showName: String? = showsInfo?.getOrNull(0)?.show?.title
return showName ?: resourceRepository.notFoundShow
}
return ""
}
}
public class ShowDetail extends UseCase {
private ShowRepository showRepository;
private String id;
@Inject
public SearchShows(ShowRepository showRepository) {
this.showRepository = showRepository;
}
public void setId(String id) {
this.id = id;
}
@Override
protected Single<Show> buildUseCaseObservable() {
Single<ShowDetail> singleDetail = showRepository.showDetail(id).subscribeOn(Schedulers.io());
Single<ShowBanner> singleBanner = showRepository.showBanner(id).subscribeOn(Schedulers.io());
return Single.zip(singleDetail, singleBanner, (detail, banner) -> new Show(detail,banner));
}
class SearchShows @Inject
constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) :
UseCase<Show>() {
var id: String? = null
override suspend fun executeOnBackground(): Show {
id?.let {
val showDetail = background{
showRepository.showDetail(it)
}
val showBanner = background{
showRepository.showBanner(it)
}
return Show(showDetail.await(), showBanner.await())
}
return Show()
}
}
public class SearchShows extends UseCase {
private ShowRepository showRepository;
private String query;
@Inject
public SearchShows(ShowRepository showRepository) {
this.showRepository = showRepository;
}
public void setQuery(String query) {
this.query = query;
}
@Override
protected Single<List<ShowResponse>> buildUseCaseObservable() {
return showRepository.searchShow(query).flatMapPublisher(
(Function<List<ShowInfo>, Flowable<ShowInfo>>) Flowable::fromIterable)
.flatMapSingle((Function<ShowInfo, SingleSource<ShowResponse>>)
showInfo -> showRepository.showRating(showInfo.getShow().getIds().getTrakt())
.map(rating -> new ShowResponse(showInfo.getShow().getTitle(), rating
.getRating())).subscribeOn(Schedulers.io()),
false, 4).toList();
}
}
class SearchShows @Inject
constructor(private val showRepository: ShowRepository) :
UseCase<List<ShowResponse>>() {
var query: String? = null
override suspend fun executeOnBackground(): List<ShowResponse> {
query?.let { query ->
return showRepository.searchShow(query).map {
background {
val rating: Rating = showRepository.showRating(it.show!!.ids!!.trakt!!)
ShowResponse(it.show.title!!, rating.rating)
}
}.map {
it.await()
}
}
return arrayListOf()
}
}
implementation 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.12.0'
public suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>?, response: Response<T?>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
continuation.resumeWithException(
NullPointerException("Response body is null: $response")
)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(t)
}
})
registerOnCompletion(continuation)
}
}
К сожалению, не доступен сервер mySQL