RxJava para Coroutines: migração de recursos de ponta a ponta

imagem

(publicado originalmente no Medium )

As corotinas Kotlin são muito mais do que apenas threads leves - são um novo paradigma que ajuda os desenvolvedores a lidar com a simultaneidade de maneira estruturada e idiomática.

Ao desenvolver um aplicativo Android, deve-se considerar muitas coisas diferentes: remover operações de longa execução do thread da interface do usuário, manipular eventos do ciclo de vida, cancelar assinaturas, retornar ao thread da interface do usuário para atualizar a interface do usuário. Nos últimos dois anos, o RxJava se tornou uma das estruturas mais usadas para resolver esse conjunto de problemas. Neste artigo, vou orientá-lo na migração de recursos de ponta a ponta do RxJava para as corotinas.

Recurso


O recurso que vamos converter em corotinas é bastante simples: quando o usuário envia um país, fazemos uma chamada de API para verificar se o país está qualificado para uma pesquisa de detalhes de negócios por meio de um provedor como o Companies House . Se a chamada foi bem-sucedida, mostramos a resposta, se não - a mensagem de erro.

Migração



Vamos migrar nosso código em uma abordagem de baixo para cima, começando com o serviço Retrofit, passando para uma camada Repository, depois para uma camada Interactor e, finalmente, para um ViewModel.

As funções que retornam atualmente Single devem se tornar funções de suspensão e as funções que retornam Observable devem retornar Flow. Neste exemplo em particular, não faremos nada com o Flows.

Serviço de modernização


Vamos pular direto para o código e refatorar o método businessLookupEligibility no BusinessLookupService para as rotinas. É assim que parece agora.

interface BusinessLookupService { @GET("v1/eligibility") fun businessLookupEligibility( @Query("countryCode") countryCode: String ): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>> } 

Etapas de refatoração:

  1. A partir da versão 2.6.0, o Retrofit suporta o modificador de suspensão. Vamos transformar o método businessLookupEligibility em uma função de suspensão.
  2. Remova o invólucro único do tipo de retorno.

 interface BusinessLookupService { @GET("v1/eligibility") suspend fun businessLookupEligibility( @Query("countryCode") countryCode: String ): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse> } 

NetworkResponse é uma classe selada que representa BusinessLookupEligibilityResponse ou ErrorResponse. O NetworkResponse é construído em um adaptador de chamada Retrofit personalizado. Dessa maneira, restringimos o fluxo de dados a apenas dois casos possíveis - sucesso ou erro, para que os clientes do BusinessLookupService não precisem se preocupar com o tratamento de exceções.

Repositório


Vamos seguir em frente e ver o que temos no BusinessLookupRepository. No corpo do método businessLookupEligibility, chamamos businessLookupService.businessLookupEligibility (aquele que acabamos de refatorar) e usamos o operador de mapa de RxJava para transformar NetworkResponse em um modelo de resposta Result e mapear para modelo de domínio. Resultado é outra classe selada que representa Result.Success e contém o objeto BusinessLookupEligibility, caso a chamada de rede tenha sido bem-sucedida. Se houve um erro na chamada de rede, uma exceção de desserialização ou outra coisa deu errado, criamos Result.Failure com uma mensagem de erro significativa (ErrorMessage é tipealias para String).

 class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val schedulerProvider: SchedulerProvider ) { fun businessLookupEligibility(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> { return businessLookupService.businessLookupEligibility(countryCode) .map { response -> return@map when (response) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } }.subscribeOn(schedulerProvider.io()) } } 

Etapas de refatoração:

  1. businessLookupEligibility se torna uma função de suspensão.
  2. Remova o invólucro único do tipo de retorno.
  3. Os métodos no repositório geralmente executam tarefas de longa execução, como chamadas de rede ou consultas de banco de dados. É de responsabilidade do repositório especificar em qual thread esse trabalho deve ser realizado. Por subscribeOn (schedulerProvider.io ()), estamos dizendo ao RxJava que o trabalho deve ser feito no encadeamento io. Como o mesmo pode ser alcançado com as corotinas? Vamos usar o withContext com um expedidor específico para mudar a execução do bloco para o encadeamento diferente e retornar ao expedidor original quando a execução for concluída. É uma boa prática garantir que uma função seja principalmente segura usando withContext. Os consumidores do BusinessLookupRepository não devem pensar em qual thread eles devem usar para executar o método businessLookupEligibility; deve ser seguro chamá-lo no thread principal.
  4. Não precisamos mais do operador de mapa, pois podemos usar o resultado de businessLookupService.businessLookupEligibility no corpo de uma função de suspensão.

 class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val dispatcherProvider: DispatcherProvider ) { suspend fun businessLookupEligibility(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = withContext(dispatcherProvider.io) { when (val response = businessLookupService.businessLookupEligibility(countryCode)) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } } } 

Interactractor


Neste exemplo específico, BusinessLookupEligibilityInteractor não contém nenhuma lógica adicional e serve como um proxy para BusinessLookupRepository. Usamos a sobrecarga do operador de chamada para que o interator possa ser chamado como uma função.

 class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> = businessLookupRepository.businessLookupEligibility(countryCode) } 

Etapas de refatoração:

  1. operador divertido chamar torna-se suspender operador divertido chamar.
  2. Remova o invólucro único do tipo de retorno.

 class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = businessLookupRepository.businessLookupEligibility(countryCode) } 

ViewModel


Em BusinessProfileViewModel, chamamos BusinessLookupEligibilityInteractor que retorna Single. Assinamos o fluxo e o observamos no thread da interface do usuário, especificando o agendador da interface do usuário. No caso de Success, atribuímos o valor de um modelo de domínio a um businessViewState LiveData. Em caso de falha, atribuímos uma mensagem de erro.

Adicionamos todas as assinaturas a um CompositeDisposable e as descartamos no método onCleared () do ciclo de vida de um ViewModel.

 class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor, private val schedulerProvider: SchedulerProvider ) : ViewModel() { private val disposables = CompositeDisposable() internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { disposables.add(businessLookupEligibilityInteractor(country.countryCode) .observeOn(schedulerProvider.ui()) .subscribe { state -> return@subscribe when (state) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } }) } @Override protected void onCleared() { super.onCleared(); disposables.clear(); } } 

Etapas de refatoração:

  1. No começo do artigo, mencionei uma das principais vantagens das corotinas - concorrência estruturada. E é aqui que entra em jogo. Toda corotina tem um escopo. O escopo tem controle sobre uma rotina por meio de seu trabalho. Se um trabalho for cancelado, todas as corotinas no escopo correspondente também serão canceladas. Você pode criar seus próprios escopos, mas, neste caso, vamos alavancar o viewModel com reconhecimento do ciclo de vida do ViewModel. Iniciaremos uma nova rotina em um viewModelScope usando viewModelScope.launch. A corotina será iniciada no encadeamento principal, pois o viewModelScope possui um expedidor padrão - Dispatchers.Main. Uma corrotina iniciada em Dispatchers.O Main não bloqueará o encadeamento principal enquanto estiver suspenso. Como acabamos de lançar uma rotina, podemos chamar o operador de suspensão businessLookupEligibilityInteractor e obter o resultado. businessLookupEligibilityInteractor chama BusinessLookupRepository.businessLookupEligibility o que muda a execução para Dispatchers.IO e volta para Dispatchers.Main. Como estamos no encadeamento da interface do usuário, podemos atualizar o businessViewState LiveData atribuindo um valor.
  2. Podemos nos livrar dos descartáveis, pois o viewModelScope está vinculado a um ciclo de vida do ViewModel. Qualquer corrotina iniciada neste escopo é automaticamente cancelada se o ViewModel for limpo.

 class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor ) : ViewModel() { internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { viewModelScope.launch { when (val state = businessLookupEligibilityInteractor(country.countryCode)) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } } } } 

Principais tópicos


Ler e entender códigos escritos com corotinas é bastante fácil, no entanto, é uma mudança de paradigma que exige algum esforço para aprender a abordar a escrita de códigos com corotinas.

Neste artigo, não cobri teste. Eu usei a biblioteca mockk porque tinha problemas ao testar corotinas usando o Mockito.

Tudo o que escrevi com o Rx Java achei bastante fácil de implementar com corotinas, fluxos e canais . Uma das vantagens das corotinas é que elas são um recurso da linguagem Kotlin e estão evoluindo junto com a linguagem.

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


All Articles