RxJava nach Coroutines: End-to-End-Feature-Migration

Bild

(ursprünglich auf Medium veröffentlicht )

Kotlin-Coroutinen sind viel mehr als nur leichte Fäden - sie sind ein neues Paradigma, das Entwicklern hilft, strukturiert und idiomatisch mit Parallelität umzugehen.

Bei der Entwicklung einer Android-App sollten viele verschiedene Aspekte berücksichtigt werden: Entfernen lang laufender Vorgänge aus dem Benutzeroberflächenthread, Behandeln von Lebenszyklusereignissen, Kündigen von Abonnements und Zurückschalten zum Benutzeroberflächenthread, um die Benutzeroberfläche zu aktualisieren. In den letzten Jahren hat sich RxJava zu einem der am häufigsten verwendeten Frameworks entwickelt, um diese Problematik zu lösen. In diesem Artikel werde ich Sie durch die End-to-End-Feature-Migration von RxJava zu Coroutinen führen.

Feature


Die Funktion, die wir in Coroutinen konvertieren werden, ist recht einfach: Wenn der Benutzer ein Land einreicht, rufen wir eine API auf, um zu prüfen, ob das Land für die Suche nach Geschäftsdetails über einen Anbieter wie Companies House geeignet ist. Wenn der Anruf erfolgreich war, zeigen wir die Antwort, wenn nicht - die Fehlermeldung.

Migration



Wir werden unseren Code in einem Bottom-up-Ansatz migrieren, beginnend mit dem Retrofit-Service, über einen Repository-Layer bis zu einem Interactor-Layer und schließlich zu einem ViewModel.

Funktionen, die derzeit Single zurückgeben, sollten zu Suspending-Funktionen werden, und Funktionen, die Observable zurückgeben, sollten Flow zurückgeben. In diesem speziellen Beispiel werden wir nichts mit Flows anfangen.

Nachrüstservice


Lassen Sie uns direkt in den Code springen und die businessLookupEligibility-Methode in BusinessLookupService an Coroutinen anpassen. So sieht es jetzt aus.

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

Refactoring-Schritte:

  1. Ab Version 2.6.0 unterstützt Retrofit den Suspend-Modifier. Lassen Sie uns die businessLookupEligibility-Methode in eine Suspending-Funktion umwandeln.
  2. Entfernen Sie die Einzelverpackung aus dem Rückgabetyp.

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

NetworkResponse ist eine versiegelte Klasse, die BusinessLookupEligibilityResponse oder ErrorResponse darstellt. NetworkResponse wird in einem benutzerdefinierten Retrofit-Anrufadapter erstellt. Auf diese Weise beschränken wir den Datenfluss auf nur zwei mögliche Fälle - Erfolg oder Fehler, sodass sich die Benutzer von BusinessLookupService nicht um die Ausnahmebehandlung kümmern müssen.

Repository


Gehen wir weiter und sehen, was wir in BusinessLookupRepository haben. Im Methodenrumpf von businessLookupEligibility rufen wir businessLookupService.businessLookupEligibility (den gerade überarbeiteten) auf und verwenden den RxJava-Kartenoperator, um NetworkResponse in ein Ergebnis- und Kartenantwortmodell in ein Domänenmodell umzuwandeln. Result ist eine weitere versiegelte Klasse, die Result.Success darstellt und das Objekt BusinessLookupEligibility enthält, falls der Netzwerkaufruf erfolgreich war. Wenn beim Netzwerkaufruf ein Fehler aufgetreten ist, eine Deserialisierungsausnahme aufgetreten ist oder etwas anderes schief gelaufen ist, erstellen wir Result.Failure mit einer aussagekräftigen Fehlermeldung (ErrorMessage ist ein typischer Alias für 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()) } } 

Refactoring-Schritte:

  1. businessLookupEligibility wird zu einer Suspend-Funktion.
  2. Entfernen Sie die Einzelverpackung aus dem Rückgabetyp.
  3. Methoden im Repository führen normalerweise lang andauernde Aufgaben wie Netzwerkaufrufe oder Datenbankabfragen aus. Es liegt in der Verantwortung des Repository, anzugeben, auf welchem ​​Thread diese Arbeit durchgeführt werden soll. Mit subscribeOn (schedulerProvider.io ()) teilen wir RxJava mit, dass am io-Thread gearbeitet werden soll. Wie könnte dasselbe mit Koroutinen erreicht werden? Wir werden withContext mit einem bestimmten Dispatcher verwenden, um die Ausführung des Blocks auf den anderen Thread und zurück zum ursprünglichen Dispatcher zu verschieben, wenn die Ausführung abgeschlossen ist. Es wird empfohlen, mithilfe von withContext sicherzustellen, dass eine Funktion main-safe ist. Benutzer von BusinessLookupRepository sollten nicht darüber nachdenken, welchen Thread sie zum Ausführen der businessLookupEligibility-Methode verwenden sollen. Es sollte sicher sein, ihn vom Hauptthread aus aufzurufen.
  4. Wir brauchen den Kartenoperator nicht mehr, da wir das Ergebnis von businessLookupService.businessLookupEligibility in einem Body einer Suspend-Funktion verwenden können.

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

Interakteur


In diesem speziellen Beispiel enthält BusinessLookupEligibilityInteractor keine zusätzliche Logik und dient als Proxy für BusinessLookupRepository. Wir verwenden die Überladung von Aufrufoperatoren , damit der Interaktor als Funktion aufgerufen werden kann.

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

Refactoring-Schritte:

  1. Der Operator-Fun-Aufruf wird zum Suspend-Operator-Fun-Aufruf.
  2. Entfernen Sie die Einzelverpackung aus dem Rückgabetyp.

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

ViewModel


In BusinessProfileViewModel rufen wir BusinessLookupEligibilityInteractor auf, der Single zurückgibt. Wir abonnieren den Stream und beobachten ihn im UI-Thread, indem wir den UI-Scheduler angeben. Im Erfolgsfall weisen wir den Wert aus einem Domain-Modell einem businessViewState LiveData zu. Im Fehlerfall weisen wir eine Fehlermeldung zu.

Wir fügen jedes Abonnement einem CompositeDisposable hinzu und entsorgen sie in der onCleared () -Methode eines ViewModel-Lebenszyklus.

 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(); } } 

Refactoring-Schritte:

  1. Am Anfang des Artikels habe ich einen der Hauptvorteile von Koroutinen erwähnt - strukturierte Parallelität. Und hier kommt es ins Spiel. Jede Koroutine hat ein Zielfernrohr. Der Scope hat über seinen Job die Kontrolle über eine Coroutine. Wenn ein Auftrag abgebrochen wird, werden auch alle Coroutinen im entsprechenden Bereich abgebrochen. Es steht Ihnen frei, Ihre eigenen Bereiche zu erstellen. In diesem Fall setzen wir jedoch den ViewModel-lebenszyklusorientierten ViewModelScope ein. Wir werden eine neue Coroutine in einem viewModelScope mit viewModelScope.launch starten. Die Coroutine wird im Haupt-Thread gestartet, da viewModelScope einen Standard-Dispatcher hat - Dispatchers.Main. Auf Dispatchern wurde eine Coroutine gestartet. Main blockiert den Haupt-Thread nicht, solange er angehalten ist. Da wir gerade eine Coroutine gestartet haben, können wir den Operator businessLookupEligibilityInteractor aufrufen, um das Ergebnis zu erhalten. businessLookupEligibilityInteractor ruft BusinessLookupRepository.businessLookupEligibility auf, wodurch die Ausführung zu Dispatchers.IO und zurück zu Dispatchers.Main verschoben wird. Da wir uns im UI-Thread befinden, können wir businessViewState LiveData aktualisieren, indem wir einen Wert zuweisen.
  2. Wir können Einwegartikel loswerden, da viewModelScope an einen ViewModel-Lebenszyklus gebunden ist. Jede in diesem Bereich gestartete Coroutine wird automatisch abgebrochen, wenn das ViewModel gelöscht wird.

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

Schlüssel zum Mitnehmen


Das Lesen und Verstehen von Code, der mit Coroutinen geschrieben wurde, ist recht einfach. Dennoch ist es ein Paradigmenwechsel, der einige Anstrengungen erfordert, um zu lernen, wie man mit Coroutinen Code schreibt.

In diesem Artikel habe ich das Testen nicht behandelt. Ich habe die Mockk- Bibliothek benutzt, da ich Probleme hatte, Coroutinen mit Mockito zu testen.

Alles, was ich mit Rx Java geschrieben habe, war mit Coroutinen, Flows und Channels ziemlich einfach zu implementieren. Einer der Vorteile von Koroutinen ist, dass sie ein Merkmal der Kotlin-Sprache sind und sich zusammen mit der Sprache entwickeln.

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


All Articles