RxJava à Coroutines: migration des fonctionnalités de bout en bout

image

(initialement publié sur Medium )

Les coroutines Kotlin sont bien plus que de simples threads légers - c'est un nouveau paradigme qui aide les développeurs à gérer la concurrence de manière structurée et idiomatique.

Lors du développement d'une application Android, il faut considérer de nombreuses choses différentes: supprimer les opérations de longue durée du thread d'interface utilisateur, gérer les événements du cycle de vie, annuler les abonnements, revenir au thread d'interface utilisateur pour mettre à jour l'interface utilisateur. Au cours des deux dernières années, RxJava est devenu l'un des cadres les plus couramment utilisés pour résoudre cet ensemble de problèmes. Dans cet article, je vais vous guider à travers la migration des fonctionnalités de bout en bout de RxJava vers les coroutines.

Fonctionnalité


La fonctionnalité que nous allons convertir en coroutines est assez simple: lorsque l'utilisateur soumet un pays, nous faisons un appel API pour vérifier si le pays est éligible pour une recherche de détails commerciaux via un fournisseur comme Companies House . Si l'appel a réussi, nous affichons la réponse, sinon - le message d'erreur.

La migration



Nous allons migrer notre code dans une approche ascendante en commençant par le service Retrofit, en passant à une couche Repository, puis à une couche Interactor et enfin à un ViewModel.

Les fonctions qui renvoient actuellement Single devraient devenir des fonctions de suspension et les fonctions qui renvoient Observable devraient renvoyer Flow. Dans cet exemple particulier, nous n'allons rien faire avec les flux.

Service de rénovation


Passons directement au code et refactorisons la méthode businessLookupElmissibilité dans BusinessLookupService en coroutines. Voilà à quoi ça ressemble maintenant.

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

Étapes de refactoring:

  1. À partir de la version 2.6.0, Retrofit prend en charge le modificateur de suspension. Transformons la méthode businessLookupElmissibilité en fonction de suspension.
  2. Supprimez le wrapper unique du type de retour.

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

NetworkResponse est une classe scellée qui représente BusinessLookupElmissibilitéResponse ou ErrorResponse. NetworkResponse est construit dans un adaptateur d'appel Retrofit personnalisé. De cette façon, nous limitons le flux de données à seulement deux cas possibles - succès ou erreur, afin que les consommateurs de BusinessLookupService n'aient pas à se soucier de la gestion des exceptions.

Dépôt


Passons à autre chose et voyons ce que nous avons dans BusinessLookupRepository. Dans le corps de la méthode businessLookupElmissibilité, nous appelons businessLookupService.businessLookupElibility (celui que nous venons de refactoriser) et utilisons l'opérateur de carte de RxJava pour transformer NetworkResponse en un résultat et mapper le modèle de réponse au modèle de domaine. Result est une autre classe scellée qui représente Result.Success et contient l'objetBusinessLookupElibility en cas de réussite de l'appel réseau. S'il y a eu une erreur dans l'appel réseau, une exception de désérialisation ou quelque chose d'autre s'est mal passé, nous avons créé Result.Failure avec un message d'erreur significatif (ErrorMessage est un typealias pour 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()) } } 

Étapes de refactoring:

  1. businessLookupElmissibilité devient une fonction de suspension.
  2. Supprimez le wrapper unique du type de retour.
  3. Les méthodes du référentiel effectuent généralement des tâches de longue durée telles que les appels réseau ou les requêtes db. Il est de la responsabilité du référentiel de spécifier sur quel thread ce travail doit être effectué. Par subscribeOn (schedulerProvider.io ()), nous disons à RxJava que le travail doit être effectué sur le thread io. Comment pourrait-on obtenir la même chose avec les coroutines? Nous allons utiliser withContext avec un répartiteur spécifique pour décaler l'exécution du bloc vers les différents threads et revenir au répartiteur d'origine une fois l'exécution terminée. C'est une bonne pratique pour vous assurer qu'une fonction est sécurisée principale en utilisant withContext. Les consommateurs de BusinessLookupRepository ne devraient pas penser au thread qu'ils devraient utiliser pour exécuter la méthode businessLookupElibility, il devrait être sûr de l'appeler à partir du thread principal.
  4. Nous n'avons plus besoin de l'opérateur de carte car nous pouvons utiliser le résultat de businessLookupService.businessLookupElibility dans un corps de fonction de suspension.

 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


Dans cet exemple spécifique, BusinessLookupElibilityInteractor ne contient aucune logique supplémentaire et sert de proxy à BusinessLookupRepository. Nous utilisons la surcharge d'opérateur d' invocation afin que l'interaction puisse être invoquée en tant que fonction.

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

Étapes de refactoring:

  1. l'opérateur fun invoke devient suspendre l'opérateur fun invoke.
  2. Supprimez le wrapper unique du type de retour.

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

ViewModel


Dans BusinessProfileViewModel, nous appelons BusinessLookupElibilityInteractor qui renvoie Single. Nous nous abonnons au flux et l'observons sur le thread d'interface utilisateur en spécifiant le planificateur d'interface utilisateur. En cas de succès, nous attribuons la valeur d'un modèle de domaine à un BusinessViewState LiveData. En cas d'échec nous attribuons un message d'erreur.

Nous ajoutons chaque abonnement à un CompositeDisposable et les éliminons dans la méthode onCleared () du cycle de vie d'un 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(); } } 

Étapes de refactoring:

  1. Au début de l'article, j'ai mentionné l'un des principaux avantages des coroutines - la concurrence structurée. Et c'est là que cela entre en jeu. Chaque coroutine a une portée. La portée a le contrôle d'une coroutine via son travail. Si un travail est annulé, toutes les coroutines de l'étendue correspondante seront également annulées. Vous êtes libre de créer vos propres étendues, mais dans ce cas, nous allons tirer parti du viewModelScope compatible avec le cycle de vie de ViewModel. Nous allons démarrer une nouvelle coroutine dans un viewModelScope en utilisant viewModelScope.launch. La coroutine sera lancée dans le thread principal car viewModelScope a un répartiteur par défaut - Dispatchers.Main. Une coroutine a démarré sur Dispatchers. Main ne bloquera pas le thread principal pendant la suspension. Comme nous venons de lancer une coroutine, nous pouvons invoquer l'opérateur de suspension businessLookupElibilityInteractor et obtenir le résultat. businessLookupElibilityInteractor appelle BusinessLookupRepository.businessLookupElibility ce qui déplace l'exécution vers Dispatchers.IO et de nouveau vers Dispatchers.Main. Comme nous sommes dans le thread d'interface utilisateur, nous pouvons mettre à jour businessViewState LiveData en attribuant une valeur.
  2. Nous pouvons nous débarrasser des produits jetables car viewModelScope est lié à un cycle de vie ViewModel. Toute coroutine lancée dans cette étendue est automatiquement annulée si le ViewModel est effacé.

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

Points clés à retenir


La lecture et la compréhension du code écrit avec des coroutines est assez facile, néanmoins c'est un changement de paradigme qui nécessite un certain effort pour apprendre à aborder l'écriture de code avec des coroutines.

Dans cet article, je n'ai pas couvert les tests. J'ai utilisé la bibliothèque mockk car j'avais des problèmes pour tester les coroutines à l'aide de Mockito.

Tout ce que j'ai écrit avec Rx Java, je l'ai trouvé assez facile à implémenter avec des coroutines, des flux et des canaux . L'un des avantages des coroutines est qu'elles sont une fonctionnalité du langage Kotlin et qu'elles évoluent avec le langage.

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


All Articles