Olá Habr! Apresento a você a tradução de um
artigo de Paulo Sato sobre o uso de Kotlin Coroutines em vez de RxJava em seus projetos Android.
RxJava como uma bazuca, a maioria dos aplicativos nĂŁo usa nem metade do seu poder de fogo. O artigo discutirá como substituĂ-lo por corotinas Kotlin (corotinas).
Trabalho com RxJava há vários anos. Esta é definitivamente uma das melhores bibliotecas para qualquer projeto Android, que ainda está em choque hoje, especialmente se você estiver programando em Java. Se você usa Kotlin, podemos dizer que a cidade tem um novo xerife.
A maioria usa o RxJava apenas para controlar threads e impedir o inferno de retorno de chamada (se vocĂŞ nĂŁo sabe o que Ă©, considere-se com sorte e
Ă© por isso ). O fato Ă© que devemos ter em mente que o verdadeiro poder do RxJava Ă© a programação reativa e a contrapressĂŁo. Se vocĂŞ o usar para controlar solicitações assĂncronas, use a bazuca para matar a aranha. Ela fará seu trabalho, mas Ă© um exagero.
Uma desvantagem notável do RxJava é o número de métodos. É enorme e tende a se espalhar por todo o código. No Kotlin, você pode usar corotinas para implementar a maior parte do comportamento criado anteriormente usando o RxJava.
Mas ... o que sĂŁo corotinas?
Corutin é uma maneira de lidar com tarefas competitivas em um thread. O encadeamento funcionará até ser interrompido e o contexto será alterado para cada corotina sem criar um novo encadeamento.
As corotinas no Kotlin ainda sĂŁo experimentais, mas estĂŁo incluĂdas no Kotlin 1.3, entĂŁo escrevi abaixo uma nova classe UseCase (para arquitetura limpa) que as utiliza. Neste exemplo, uma chamada de rotina Ă© encapsulada em um Ăşnico arquivo. Assim, outras camadas nĂŁo dependerĂŁo da execução das corotinas, proporcionando uma arquitetura mais 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() } }
Antes de tudo, criei uma tarefa pai. Essa é a chave para desfazer todas as corotinas que foram criadas na classe UseCase. Quando chamamos execução, é importante que as tarefas antigas sejam canceladas para garantir que não perdemos uma única rotina (isso também acontecerá se cancelarmos a inscrição neste UseCase).
AlĂ©m disso, invoco a inicialização (UI). Isso significa que eu quero criar uma rotina que será executada no thread da interface do usuário. Depois disso, chamo o mĂ©todo de segundo plano que cria assĂncrono no CommonPool (essa abordagem realmente terá um desempenho ruim). Por sua vez, o async retornará Deffered e, entĂŁo, chamarei seu mĂ©todo de espera. Ele espera pela conclusĂŁo da rotina em segundo plano, o que trará um resultado ou erro.
Isso pode ser usado para implementar quase tudo que fizemos com o RxJava. Abaixo estĂŁo alguns exemplos.
Mapa
Fiz o download dos resultados do searchShow e os alterei para retornar o nome do primeiro programa.
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 da Corotina:
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
O Zip pega duas emissões do Observer e as reúne em uma nova emissão. Observe que com o RxJava você deve especificar para fazer uma chamada em paralelo usando subscribeOn em cada single. Queremos obter os dois ao mesmo tempo e devolvê-los 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 da Corotina:
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() } }
Flatmap
Nesse caso, estou procurando programas que tenham uma string de consulta e, para cada resultado (limitado a 200 resultados), também recebo a classificação do programa. No final, retorno uma lista de programas com as classificações correspondentes.
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 da Corotina:
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() } }
Deixe-me explicar. Usando o RxJava, meu repositório retorna uma única emissão de List, então eu preciso de várias emissões, uma para cada ShowInfo. Para fazer isso, chamei flatMapPublisher. Para cada edição, tenho que destacar o ShowResponse e, no final, coletar todos eles em uma lista.
Terminamos com esta construção: List foreach → (ShowInfo → ShowRating → ShowResponse) → List.
Com corotinas, fiz um mapa para cada elemento da Lista para convertĂŞ-lo em uma Lista <Defferida>.
Como vocĂŞ pode ver, a maior parte do que fizemos com o RxJava Ă© mais fácil de implementar com chamadas sĂncronas. As corotinas podem atĂ© manipular o flatMap, que, acredito, Ă© uma das funções mais complexas do RxJava.
É sabido que as corotinas podem ser leves (
aqui está um exemplo), mas os resultados me intrigaram. Neste exemplo, o RxJava iniciou em cerca de 3,1 segundos, enquanto as corotinas levaram cerca de 5,8 segundos para executar no CommonPool.
Esses resultados levantaram a questĂŁo diante de mim de que poderia haver algo inapropriado neles. Mais tarde, encontrei isso. Usei o retrofit Call, que bloqueou o fluxo.
Há duas maneiras de corrigir esse erro. A escolha depende da versão do Android Studio que você está usando. No Android Studio 3.1, precisamos garantir que não estamos bloqueando o encadeamento em segundo plano. Para isso, usei esta biblioteca:
implementação 'ru.gildor.coroutines: kotlin-coroutines-retrofit: 0.12.0'
Este código cria uma extensão da função Retrofit Call para pausar o fluxo:
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) {
No Android Studio 3.2, você pode atualizar a biblioteca corutin para a versão 0.25.0. Esta versão possui CoroutineContext IO (você pode ver o comentário correspondente na minha classe UseCase).
A execução no CommonPool sem uma chamada de bloqueio levou 2,3 ​​segundos e 2,4 segundos com E / S e chamadas de bloqueio.

Espero que este artigo o inspire a usar o corutin, uma alternativa mais leve e talvez mais rápida ao RxJava e torne um pouco mais fácil entender que vocĂŞ está escrevendo cĂłdigo sincronizado que Ă© executado de forma assĂncrona.