RxJava a Coroutines: migración de características de extremo a extremo

imagen

(publicado originalmente en Medium )

Las rutinas de Kotlin son mucho más que simples hilos livianos: son un nuevo paradigma que ayuda a los desarrolladores a lidiar con la concurrencia de una manera estructurada e idiomática.

Al desarrollar una aplicación de Android, se deben considerar muchas cosas diferentes: quitar operaciones de larga duración del hilo de la interfaz de usuario, manejar eventos del ciclo de vida, cancelar suscripciones, volver al hilo de la interfaz de usuario para actualizar la interfaz de usuario. En los últimos años, RxJava se convirtió en uno de los marcos más utilizados para resolver este conjunto de problemas. En este artículo, lo guiaré a través de la migración de características de extremo a extremo de RxJava a las rutinas.

Característica


La función que vamos a convertir en corutinas es bastante simple: cuando el usuario envía un país, hacemos una llamada API para verificar si el país es elegible para una búsqueda de detalles comerciales a través de un proveedor como Companies House . Si la llamada fue exitosa, mostramos la respuesta, si no, el mensaje de error.

Migración



Vamos a migrar nuestro código en un enfoque ascendente comenzando con el servicio Retrofit, pasando a una capa de Repositorio, luego a una capa de Interactor y finalmente a un Modelo de Vista.

Las funciones que actualmente devuelven Single deberían convertirse en funciones de suspensión y las funciones que devuelven Observable deberían devolver Flow. En este ejemplo en particular, no vamos a hacer nada con Flows.

Servicio de modernización


Pasemos directamente al código y refactoricemos el método businessLookupEligibility en BusinessLookupService a las rutinas. Así es como se ve ahora.

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

Pasos de refactorización:

  1. A partir de la versión 2.6.0, Retrofit admite el modificador de suspensión. Convirtamos el método businessLookupEligibility en una función de suspensión.
  2. Retire el contenedor individual del tipo de retorno.

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

NetworkResponse es una clase sellada que representa BusinessLookupEligibilityResponse o ErrorResponse. NetworkResponse se construye en un adaptador de llamada Retrofit personalizado. De esta forma, restringimos el flujo de datos a solo dos casos posibles: éxito o error, de modo que los consumidores de BusinessLookupService no tengan que preocuparse por el manejo de excepciones.

Repositorio


Continuemos y veamos lo que tenemos en BusinessLookupRepository. En el cuerpo del método businessLookupEligibility llamamos a businessLookupService.businessLookupEligibility (el que acabamos de refactorizar) y utilizamos el operador de mapa de RxJava para transformar NetworkResponse en un modelo de respuesta de resultado y mapa al modelo de dominio. Result es otra clase sellada que representa Result.Success y contiene el objetoBusinessLookupEligibility en caso de que la llamada a la red haya sido exitosa. Si hubo un error en la llamada de red, la excepción de deserialización o algo más salió mal, construimos Result.Failure con un mensaje de error significativo (ErrorMessage es typealias 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()) } } 

Pasos de refactorización:

  1. businessLookupEligibility se convierte en una función de suspensión.
  2. Retire el contenedor individual del tipo de retorno.
  3. Los métodos en el repositorio generalmente realizan tareas de larga duración, como llamadas de red o consultas db. Es responsabilidad del repositorio especificar en qué hilo se debe realizar este trabajo. Por subscribeOn (SchedulerProvider.io ()) le estamos diciendo a RxJava que el trabajo debe hacerse en el hilo io. ¿Cómo podría lograrse lo mismo con las corutinas? Vamos a usar withContext con un despachador específico para cambiar la ejecución del bloque a los diferentes subprocesos y volver al despachador original cuando se complete la ejecución. Es una buena práctica asegurarse de que una función sea segura para usar maincontext. Los consumidores de BusinessLookupRepository no deberían pensar qué hilo deberían usar para ejecutar el método businessLookupEligibility, debería ser seguro llamarlo desde el hilo principal.
  4. Ya no necesitamos el operador de mapa, ya que podemos usar el resultado de businessLookupService.businessLookupEligibility en un cuerpo de una función de suspensión.

 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


En este ejemplo específico, BusinessLookupEligibilityInteractor no contiene ninguna lógica adicional y sirve como un proxy para BusinessLookupRepository. Utilizamos la sobrecarga del operador de invocación para que se pueda invocar al interactor como una función.

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

Pasos de refactorización:

  1. invocar diversión del operador se convierte en suspender invocación divertida del operador.
  2. Retire el contenedor individual del tipo de retorno.

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

ViewModel


En BusinessProfileViewModel llamamos BusinessLookupEligibilityInteractor que devuelve Single. Nos suscribimos a la secuencia y la observamos en el subproceso de la interfaz de usuario especificando el planificador de la interfaz de usuario. En caso de éxito, asignamos el valor de un modelo de dominio a un BusinessViewState LiveData. En caso de falla, asignamos un mensaje de error.

Agregamos todas las suscripciones a CompositeDisposable y las desechamos en el método onCleared () del ciclo de vida de 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(); } } 

Pasos de refactorización:

  1. Al comienzo del artículo, mencioné una de las principales ventajas de las corutinas: la concurrencia estructurada. Y aquí es donde entra en juego. Cada corutina tiene un alcance. El alcance tiene control sobre una corutina a través de su trabajo. Si se cancela un trabajo, también se cancelarán todas las rutinas en el ámbito correspondiente. Usted es libre de crear sus propios ámbitos, pero en este caso vamos a aprovechar viewModel lifecycle-aware viewModelScope. Comenzaremos una nueva rutina en viewModelScope usando viewModelScope.launch. La rutina se lanzará en el hilo principal ya que viewModelScope tiene un despachador predeterminado: Dispatchers.Main. Se inició una rutina en los Despachadores. Main no bloqueará el hilo principal mientras esté suspendido. Como acabamos de lanzar una rutina, podemos invocar al operador de suspensión businessLookupEligibilityInteractor y obtener el resultado. businessLookupEligibilityInteractor llama a BusinessLookupRepository.businessLookupEligibility lo que transfiere la ejecución a Dispatchers.IO y nuevamente a Dispatchers.Main. Como estamos en el hilo de la interfaz de usuario, podemos actualizar businessViewState LiveData asignando un valor.
  2. Podemos deshacernos de los elementos desechables ya que viewModelScope está vinculado a un ciclo de vida de ViewModel. Cualquier rutina lanzada en este ámbito se cancela automáticamente si se borra el ViewModel.

 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 } } } } 

Para llevar clave


Leer y comprender el código escrito con corutinas es bastante fácil, sin embargo, es un cambio de paradigma que requiere un poco de esfuerzo para aprender cómo abordar la escritura de código con corutinas.

En este artículo no cubrí las pruebas. Utilicé la biblioteca mockk porque tuve problemas para probar las corutinas con Mockito.

Todo lo que he escrito con Rx Java lo encontré bastante fácil de implementar con corutinas, flujos y canales . Una de las ventajas de las corutinas es que son una característica del lenguaje Kotlin y están evolucionando junto con el lenguaje.

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


All Articles