Hola Habr! Les presento una traducción de un
artículo de Paulo Sato sobre el uso de Kotlin Coroutines en lugar de RxJava en sus proyectos de Android.
RxJava como una bazuca, la mayoría de las aplicaciones no utilizan ni la mitad de su potencia de fuego. El artículo discutirá cómo reemplazarlo con corotinas Kotlin (corutinas).
He estado trabajando con RxJava durante varios años. Esta es definitivamente una de las mejores bibliotecas para cualquier proyecto de Android, que todavía está en estado de shock hoy, especialmente si está programando en Java. Si usa Kotlin, entonces podemos decir que la ciudad tiene un nuevo sheriff.
La mayoría usa RxJava solo para controlar subprocesos y evitar el infierno de devolución de llamadas (si no sabe lo que es, considérese afortunado y
es por eso ). El hecho es que debemos tener en cuenta que el poder real de RxJava es la programación reactiva y la contrapresión. Si lo usa para controlar solicitudes asincrónicas, usa la bazuca para matar a la araña. Ella hará su trabajo, pero es excesivo.
Un inconveniente notable de RxJava es la cantidad de métodos. Es enorme y tiende a extenderse por todo el código. En Kotlin, puede usar corutinas para implementar la mayoría del comportamiento que creó anteriormente con RxJava.
Pero ... ¿qué son las corutinas?
Corutin es una forma de manejar tareas competitivas en un hilo. El hilo funcionará hasta que se detenga y el contexto cambiará para cada rutina sin crear un nuevo hilo.
Las corutinas en Kotlin todavía son experimentales, pero están incluidas en Kotlin 1.3, así que escribí a continuación una nueva clase UseCase (para arquitectura limpia) que las usa. En este ejemplo, una llamada de rutina se encapsula en un solo archivo. Por lo tanto, otras capas no dependerán de las rutinas que se ejecutan, proporcionando una arquitectura más desconectada.
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 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() } }
En primer lugar, creé una tarea principal. Esta es la clave para deshacer todas las rutinas que se crearon en la clase UseCase. Cuando llamamos a ejecución, es importante que se cancelen las tareas antiguas para asegurarse de que no nos hemos perdido una sola rutina (esto también sucederá si cancelamos la suscripción a este UseCase).
Además, invoco el inicio (UI). Esto significa que quiero crear una rutina que se ejecutará en el hilo de la interfaz de usuario. Después de eso, llamo al método de fondo que crea asíncrono en CommonPool (este enfoque en realidad tendrá un bajo rendimiento). A su vez, async devolverá diferido, y luego, llamaré a su método de espera. Espera a que se complete la rutina de fondo, lo que traerá un resultado o error.
Esto se puede usar para implementar la mayoría de todo lo que hicimos con RxJava. A continuación hay algunos ejemplos.
Mapa
Descargué los resultados de searchShow y los cambié para devolver el nombre del primer show.
Código RxJava:
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(); } }); } }
Código de rutina:
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 "" } }
ZIP
Zip tomará dos emisiones de Observer y las juntará en una nueva emisión. Tenga en cuenta que con RxJava debe especificar hacer una llamada en paralelo usando subscribeOn en cada Single. Queremos obtener ambos al mismo tiempo y devolverlos juntos.
Código RxJava:
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)); }
Código de rutina:
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() } }
Mapa plano
En este caso, estoy buscando programas que tengan una cadena de consulta y para cada resultado (limitado a 200 resultados), también obtengo la calificación del programa. Al final, devuelvo una lista de espectáculos con las calificaciones correspondientes.
Código RxJava:
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(); } }
Código de rutina:
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() } }
Déjame explicarte. Usando RxJava, mi repositorio devuelve una única emisión de Lista, por lo que necesito varias emisiones, una para cada ShowInfo. Para hacer esto, llamé a flatMapPublisher. Para cada tema, tengo que resaltar ShowResponse, y al final reunirlos todos en una lista.
Terminamos con esta construcción: Lista foreach → (ShowInfo → ShowRating → ShowResponse) → Lista.
Con las rutinas, hice un mapa para cada elemento de la Lista para convertirlo en una Lista <Deffered>.
Como puede ver, la mayoría de lo que hicimos con RxJava es más fácil de implementar con llamadas sincrónicas. Las rutinas incluso pueden manejar flatMap, que, creo, es una de las funciones más complejas de RxJava.
Es bien sabido que las corutinas pueden ser livianas (
aquí hay un ejemplo), pero los resultados me dejaron perplejo. En este ejemplo, RxJava se inició en aproximadamente 3,1 segundos, mientras que las rutinas tardaron aproximadamente 5,8 segundos en ejecutarse en CommonPool.
Estos resultados me plantearon la pregunta de que podría haber algo inapropiado en ellos. Más tarde, encontré esto. Utilicé la llamada de modificación, que bloqueó el flujo.
Hay dos formas de corregir este error, la elección depende de la versión de Android Studio que esté utilizando. En Android Studio 3.1, debemos asegurarnos de que no estamos bloqueando el hilo de fondo. Para esto, utilicé esta biblioteca:
implementación 'ru.gildor.coroutines: kotlin-coroutines-retrofit: 0.12.0'
Este código crea una extensión de la función de llamada de actualización para pausar la transmisión:
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) {
En Android Studio 3.2, puede actualizar la biblioteca de corutin a la versión 0.25.0. Esta versión tiene CoroutineContext IO (puede ver el comentario correspondiente en mi clase UseCase).
La ejecución en CommonPool sin una llamada de bloqueo tomó 2.3 segundos y 2.4 segundos con IO y llamadas de bloqueo.

Espero que este artículo lo inspire a usar corutina, una alternativa más ligera y quizás más rápida a RxJava, y que le resulte un poco más fácil entender que está escribiendo código sincronizado que se ejecuta de forma asincrónica.