Como substituí o RxJava por corotinas no meu projeto e por que você provavelmente também deve fazer isso

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.

/** * (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() } } 

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) { // Don't bother with resuming the continuation if it is already cancelled. if (continuation.isCancelled) return continuation.resumeWithException(t) } }) registerOnCompletion(continuation) } } 

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.

imagem

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.

Source: https://habr.com/ru/post/pt421739/


All Articles