Kotlin Coroutines vs RxJava dla sieci i pracy w tle
Kotlin Coroutines vs RxJava: porównanie anulowania, obsługi błędów i wzorców testowych dla połączeń sieciowych i pracy w tle w realnych aplikacjach Android.

Dlaczego ten wybór ma znaczenie dla produkcyjnego ruchu sieciowego
Sieć i praca w tle w prawdziwej aplikacji Android to coś więcej niż pojedyncze wywołanie API. To logowanie i odświeżanie tokena, ekrany, które mogą zostać obrócone w trakcie ładowania, synchronizacja po opuszczeniu ekranu, wysyłanie zdjęć i okresowe zadania, które nie mogą nadmiernie rozładowywać baterii.
Błędy, które najbardziej doskwierają, zwykle nie wynikają z kwestii składni. Pojawiają się, gdy praca asynchroniczna żyje dłużej niż UI (wycieki), gdy anulowanie zatrzymuje UI, ale nie rzeczywiste żądanie (zmarnowany ruch i utknięte wskaźniki ładowania), gdy retry mnożą zapytania (limity, bany), albo gdy różne warstwy inaczej obsługują błędy i nikt nie potrafi przewidzieć, co zobaczy użytkownik.
Decyzja Kotlin Coroutines vs RxJava wpływa na codzienną niezawodność:
- Jak modelujesz pracę (jednorazowe wywołania vs strumienie)
- Jak propaguje się anulowanie
- Jak błędy są reprezentowane i przekazywane do UI
- Jak kontrolujesz wątki dla sieci, dysku i UI
- Jak testowalne są timingi, retry i przypadki brzegowe
Poniższe wzorce koncentrują się na tym, co zwykle psuje się pod obciążeniem lub w wolnych sieciach: anulowanie, obsługa błędów, retry i timeouty oraz nawyki testowe, które zapobiegają regresjom. Przykłady są krótkie i praktyczne.
Podstawowe modele myślowe: wywołania suspend, strumienie i Flow
Główna różnica między Kotlin Coroutines vs RxJava to kształt pracy, którą modelujesz.
Funkcja suspend reprezentuje operację jednorazową. Zwraca jedną wartość albo rzuca jedno niepowodzenie. Pasuje to do większości wywołań sieciowych: pobierz profil, zaktualizuj ustawienia, prześlij zdjęcie. Kod wołający czyta od góry do dołu, co pozostaje czytelne nawet po dodaniu logowania, cache'owania i rozgałęzień.
RxJava zaczyna od pytania, czy masz do czynienia z jedną wartością, czy wieloma wartościami w czasie. Single to wynik jednorazowy (sukces lub błąd). Observable (lub Flowable) to strumień, który może emitować wiele wartości, a potem się zakończyć lub zawieść. To pasuje do funkcji, które są naprawdę zdarzeniowe: zmiany tekstu, wiadomości websocket, polling.
Flow to podejście zorientowane na korutyny do reprezentowania strumienia. Myśl o nim jak o „wersji strumieniowej” korutyn, z ustrukturyzowanym anulowaniem i bezpośrednim dopasowaniem do API zawierających suspend.
Szybka zasada:
- Używaj
suspenddla jednego żądania i jednej odpowiedzi. - Używaj
Flowdla wartości, które zmieniają się w czasie. - Użyj RxJava, gdy Twoja aplikacja już silnie polega na operatorach i złożonej kompozycji strumieni.
W miarę rozwoju funkcji czytelność zwykle psuje się najpierw, gdy na siłę wymusisz model strumieniowy dla jednorazowego wywołania, albo gdy próbujesz traktować bieżące zdarzenia jak pojedynczą zwracaną wartość. Najpierw dopasuj abstrakcję do rzeczywistości, potem buduj konwencje.
Anulowanie w praktyce (z krótkimi przykładami kodu)
Anulowanie to moment, w którym kod asynchroniczny albo działa „bezpiecznie”, albo zamienia się w losowe awarie i zmarnowane wywołania. Cel jest prosty: kiedy użytkownik opuszcza ekran, praca uruchomiona dla tego ekranu powinna się zatrzymać.
W Kotlin Coroutines anulowanie jest wbudowane w model. Job reprezentuje pracę, a dzięki ustrukturyzowanej współbieżności zwykle nie przekazujesz Jobów. Uruchamiasz pracę wewnątrz scope'a (np. viewModelScope). Gdy ten scope zostanie anulowany, wszystko wewnątrz też.
class ProfileViewModel(
private val api: Api
) : ViewModel() {
fun loadProfile() = viewModelScope.launch {
// If the ViewModel is cleared, this coroutine is cancelled,
// and so is the in-flight network call (if the client supports it).
val profile = api.getProfile() // suspend
// update UI state here
}
}
Dwa produkcyjne szczegóły mają znaczenie:
- Wywołuj sieć przez klienta wspierającego anulowanie. W przeciwnym razie korutyna się zatrzyma, ale wywołanie HTTP może dalej działać.
- Używaj
withTimeout(lubwithTimeoutOrNull) dla żądań, które nie mogą wisieć.
RxJava używa jawnego disposalu. Trzymasz Disposable dla każdej subskrypcji lub zbierasz je w CompositeDisposable. Gdy ekran znika, disposujesz — i łańcuch powinien się zatrzymać.
class ProfilePresenter(private val api: ApiRx) {
private val bag = CompositeDisposable()
fun attach() {
bag += api.getProfile()
.subscribe(
{ profile -> /* render */ },
{ error -> /* show error */ }
)
}
fun detach() {
bag.clear() // cancels in-flight work if upstream supports cancellation
}
}
Praktyczna reguła przy opuszczaniu ekranu: jeśli nie potrafisz wskazać, gdzie odbywa się anulowanie (anulowanie scope'a lub dispose()), załóż, że praca będzie dalej działać i napraw to zanim wypuścisz aplikację.
Obsługa błędów, która pozostaje zrozumiała
Duża różnica między Kotlin Coroutines vs RxJava to sposób, w jaki błędy płyną. Korutyny sprawiają, że awarie wyglądają jak normalny kod: wywołanie suspend rzuca wyjątek, a wywołujący decyduje, co zrobić. Rx przesuwa awarie przez strumień — to potężne, ale łatwo ukryć problemy, jeśli nie dbasz o ścieżkę błędu.
Używaj wyjątków dla nieoczekiwanych awarii (timeouty, 500, błędy parsowania). Modeluj błędy jako dane, gdy UI potrzebuje konkretnej odpowiedzi (złe hasło, „email już użyty”) i chcesz, żeby była częścią modelu domenowego.
Prosty wzorzec z korutynami zachowuje stack trace i pozostaje czytelny:
suspend fun loadProfile(): Profile = try {
api.getProfile() // may throw
} catch (e: IOException) {
throw NetworkException("No connection", e)
}
runCatching i Result są przydatne, gdy naprawdę chcesz zwracać sukces lub porażkę bez rzucania:
suspend fun loadProfileResult(): Result<Profile> =
runCatching { api.getProfile() }
Uważaj na getOrNull(), jeśli nie obsługujesz porażki — to może cicho zamienić rzeczywiste błędy w ekrany "pustego" stanu.
W RxJava trzymaj ścieżkę błędów jawną. Używaj onErrorReturn tylko dla bezpiecznych fallbacków. Preferuj onErrorResumeNext, gdy musisz zmienić źródło (np. na dane z cache). Dla retry trzymaj reguły wąskie z retryWhen, żeby nie powtarzać żądań przy „złym haśle”.
Zestaw nawyków, które zapobiegają utopieniu błędów:
- Zaloguj lub zgłoś błąd raz, blisko miejsca, gdzie masz kontekst.
- Zachowaj oryginalny wyjątek jako
causeprzy opakowywaniu. - Unikaj catch-all fallbacków, które zamieniają każdy błąd w wartość domyślną.
- Uczyń błędy widoczne dla użytkownika jako typowany model, nie zwykły string.
Podstawy wątków: Dispatchers vs Schedulers
Wiele błędów asynchronicznych sprowadza się do wątków: wykonujesz ciężką pracę na głównym wątku albo dotykasz UI z wątku tła. Kotlin Coroutines vs RxJava różnią się głównie sposobem wyrażenia przełączania wątków.
W korutynach często startujesz na wątku głównym dla pracy UI, potem przeskakujesz na dispatcher do pracy w tle. Typowe wybory:
Dispatchers.Maindo aktualizacji UIDispatchers.IOdo blokującego I/O jak sieć i dyskDispatchers.Defaultdo pracy CPU jak parsowanie JSON, sortowanie, szyfrowanie
Prosty wzorzec: pobierz dane, sparsuj poza main, potem renderuj.
viewModelScope.launch(Dispatchers.Main) {
val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
val profile = withContext(Dispatchers.Default) { parseProfile(json) }
_uiState.value = UiState.Content(profile)
}
RxJava wyraża „gdzie wykonuje się praca” przez subscribeOn i „gdzie obserwowane są wyniki” przez observeOn. Częstym zaskoczeniem jest oczekiwanie, że observeOn wpłynie na upstream — nie wpływa. subscribeOn ustawia wątek dla źródła i operatorów nad nim, a każde observeOn przełącza wątek od tego punktu dalej.
api.fetchProfileJson()
.subscribeOn(Schedulers.io())
.map { json -> parseProfile(json) } // still on io unless you change it
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ profile -> render(profile) },
{ error -> showError(error) }
)
Reguła, która unika niespodzianek: trzymaj pracę UI w jednym miejscu. W korutynach przypisuj lub zbieraj stan UI na Dispatchers.Main. W RxJava dodaj jedno końcowe observeOn(main) tuż przed renderowaniem i nie rozsypuj dodatkowych observeOn, chyba że naprawdę ich potrzebujesz.
Jeśli ekran zacina się, najpierw przenieś parsowanie i mapowanie z main na wątki w tle. Ta jedna zmiana rozwiązuje wiele problemów w realnym świecie.
Retry, timeouty i praca równoległa dla wywołań sieciowych
Szczęśliwa ścieżka rzadko jest problemem. Problemy pochodzą z wywołań, które wiszą, retry, które pogarszają sytuację, lub „równoległej” pracy, która w rzeczywistości nie jest równoległa. Te wzorce często decydują, czy zespół woli Kotlin Coroutines czy RxJava.
Timeouty, które kończą się szybko
W korutynach możesz nałożyć twardą granicę wokół dowolnego wywołania suspend. Trzymaj timeout blisko miejsca wywołania, by móc pokazać odpowiedni komunikat UI.
val user = withTimeout(5_000) {
api.getUser() // suspend
}
W RxJava dodajesz operator timeout do strumienia. To przydatne, gdy zachowanie timeoutu ma być częścią wspólnego potoku.
Retry bez szkód
Retryuj tylko wtedy, gdy jest to bezpieczne. Prosta reguła: retryuj swobodniej żądania idempotentne (jak GET) niż żądania tworzące skutki uboczne (jak „utwórz zamówienie”). Nawet wtedy ogranicz liczbę prób i dodaj opóźnienie lub jitter.
Dobre domyślne zabezpieczenia:
- Retryuj przy timeoutach sieciowych i tymczasowych błędach serwera.
- Nie retryuj przy błędach walidacji (400) ani przy błędach uwierzytelnienia.
- Ogranicz retry (często 2–3) i zaloguj ostateczną porażkę.
- Używaj backoffu, by nie obciążać serwera.
W RxJava retryWhen pozwala wyrazić „retry tylko dla tych błędów, z tym opóźnieniem”. W korutynach Flow są retry i retryWhen, podczas gdy w zwykłych funkcjach suspend często stosuje się małą pętlę z delay.
Równoległe wywołania bez poplątania kodu
Korutyny czynią pracę równoległą prostą: uruchom dwa żądania, poczekaj na oba.
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
RxJava błyszczy, gdy łączenie wielu źródeł jest głównym celem łańcucha. zip to zwykłe „poczekaj na oba”, a merge jest użyteczne, gdy chcesz wyników, gdy tylko nadchodzą.
Dla dużych lub szybkich strumieni backpressure nadal ma znaczenie. Flowable w RxJava ma dojrzałe narzędzia backpressure. Coroutines Flow radzi sobie w wielu przypadkach, ale nadal możesz potrzebować buforowania lub polityk pomijania, jeśli zdarzenia przewyższają UI lub zapisy do bazy.
Interoperacyjność i wzorce migracji (mieszane codebase'y)
Większość zespołów nie przechodzi natychmiastowo. Praktyczna migracja Kotlin Coroutines vs RxJava utrzymuje aplikację stabilną, gdy przenosisz moduł po module.
Opakuj API Rx w funkcję suspend
Jeśli masz istniejące Single<T> lub Completable, opakuj je z obsługą anulowania, tak by anulowana korutyna disposowała subskrypcję Rx.
suspend fun <T : Any> Single<T>.awaitCancellable(): T =
suspendCancellableCoroutine { cont ->
val d = subscribe(
{ value -> cont.resume(value) {} },
{ error -> cont.resumeWithException(error) }
)
cont.invokeOnCancellation { d.dispose() }
}
To unika powszechnego trybu awaryjnego: użytkownik opuszcza ekran, korutyna jest anulowana, ale wywołanie sieciowe nadal działa i później aktualizuje współdzielony stan.
Udostępnij kod korutynowy wywołującemu Rx
Podczas migracji niektóre warstwy będą dalej oczekiwać typów Rx. Owiń pracę suspend w Single.fromCallable i blokuj tylko na wątku w tle.
fun loadProfileRx(api: Api): Single<Profile> =
Single.fromCallable {
runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
}
Utrzymuj tę granicę małą i udokumentowaną. Dla nowego kodu preferuj bezpośrednie wywołania suspend z zakresu korutyn.
Gdzie Flow pasuje, a gdzie nie
Flow może zastąpić wiele przypadków użycia Observable: stan UI, aktualizacje bazy danych i strumienie podobne do paging. Może być mniej bezpośredni, jeśli silnie polegasz na hot streamach, subjects, zaawansowanym tuningu backpressure lub dużym zestawie custom operatorów, które zespół już zna.
Strategia migracji zmniejszająca zamieszanie:
- Konwertuj najpierw moduły liściaste (sieć, storage) na suspend API.
- Dodaj małe adaptery na granicach modułów (Rx -> suspend, suspend -> Rx).
- Zastępuj strumienie Rx Flow tam, gdzie kontrolujesz też konsumentów.
- Trzymaj jeden styl asynchroniczny na obszar funkcjonalny.
- Usuń adaptery, gdy ostatni caller się zmigruje.
Wzorce testowe, których rzeczywiście będziesz używać
Problemy z timingu i anulowaniem to miejsca, gdzie kryją się błędy asynchroniczne. Dobre testy asynchroniczne czynią czas deterministycznym i wyniki łatwymi do asercji. To kolejna różnica Kotlin Coroutines vs RxJava, choć oba style da się dobrze testować.
Korutyny: runTest, TestDispatcher i kontrola czasu
Dla kodu korutynowego preferuj runTest z test dispatcherem, by test nie zależał od rzeczywistych wątków czy opóźnień. Wirtualny czas pozwala wyzwalać timeouty, retry i okna debounce bez usypiania testu.
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `emits Loading then Success`() = runTest {
val dispatcher = StandardTestDispatcher(testScheduler)
val repo = Repo(api = fakeApi, io = dispatcher)
val states = mutableListOf<UiState>()
val job = launch(dispatcher) { repo.loadProfile().toList(states) }
testScheduler.runCurrent() // run queued work
assert(states.first() is UiState.Loading)
testScheduler.advanceTimeBy(1_000) // trigger delay/retry windows
testScheduler.runCurrent()
assert(states.last() is UiState.Success)
job.cancel()
}
Aby przetestować anulowanie, anuluj kolekcjonujący Job (lub scope rodzica) i sprawdź, że twój fake API zatrzymuje pracę lub nie emituje już więcej stanów.
RxJava: TestScheduler, TestObserver, deterministyczny czas
Testy Rx zwykle łączą TestScheduler dla czasu i TestObserver dla asercji.
@Test
fun `disposes on cancel and stops emissions`() {
val scheduler = TestScheduler()
val observer = TestObserver<UiState>()
val d = repo.loadProfileRx(scheduler)
.subscribeWith(observer)
scheduler.triggerActions()
observer.assertValueAt(0) { it is UiState.Loading }
d.dispose()
scheduler.advanceTimeBy(1, TimeUnit.SECONDS)
observer.assertValueCount(1) // no more events after dispose
}
Testując ścieżki błędów w obu stylach, skup się na mapowaniu, nie na typie wyjątku. Sprawdzaj stan UI, którego oczekujesz po 401, timeoutie czy niepoprawnej odpowiedzi.
Mały zestaw kontroli obejmuje większość regresji:
- Stany Loading i końcowe (Success, Empty, Error)
- Sprzątanie po anulowaniu (job anulowany, disposable disposed)
- Mapowanie błędów (kody serwera na komunikaty użytkownika)
- Brak duplikatów emisji po retry
- Logika zależna od czasu przy użyciu wirtualnego czasu, nie rzeczywistych opóźnień
Typowe błędy, które prowadzą do bugów produkcyjnych
Większość problemów produkcyjnych nie wynika z wyboru Kotlin Coroutines vs RxJava. Pochodzą z kilku nawyków, które powodują, że praca działa dłużej niż myślisz, działa dwukrotnie lub dotyka UI w złym wątku.
Jedno typowe wycieknięcie to uruchamianie pracy w złym scope'ie. Jeśli zaczynasz żądanie z scope'a, który żyje dłużej niż ekran (albo tworzysz własny scope i nigdy go nie anulujesz), żądanie może zakończyć się po opuszczeniu ekranu i nadal próbować zaktualizować stan. W korutynach często wygląda to jak domyślne użycie długowiecznego scope'a. W RxJava zwykle jest to zapomniany dispose.
Kolejny klasyk to „fire and forget”. Globalne scope'y i zapomniane Disposables wydają się w porządku, dopóki praca się nie nawarstwi. Ekran czatu, który odświeża się przy każdym resume, może łatwo uruchomić wiele zadań odświeżania po kilku nawigacjach, z których każdy trzyma pamięć i konkuruje o sieć.
Retry też łatwo zepsuć. Nieograniczone retry lub retry bez opóźnienia mogą zasypać backend i rozładować baterię. Szczególnie niebezpieczne, gdy błąd jest permanentny, np. 401 po wylogowaniu. Uczyń retry warunkowym, dodaj backoff i przestań, gdy błąd nie jest możliwy do naprawienia.
Błędy związane z wątkami powodują awarie trudne do odtworzenia. Możesz parsować JSON na main albo aktualizować UI z wątku tła, w zależności od tego, gdzie umieścisz dispatcher lub scheduler.
Szybkie kontrole, które łapią większość problemów:
- Powiąż pracę z właścicielem lifecycle i anuluj ją po zakończeniu właściciela.
- Uczyń sprzątanie oczywistym: anuluj Joby lub czyść Disposables w jednym miejscu.
- Nałóż ścisłe limity na retry (liczba, opóźnienie i które błędy kwalifikują się).
- Wymuszaj jedną regułę dla aktualizacji UI (tylko wątek główny) podczas code review.
- Traktuj synchronizację w tle jako system z ograniczeniami, nie jako przypadkowe wywołanie funkcji.
Jeśli wypuszczasz aplikacje Android z generowanego kodu Kotlin (np. z AppMaster), te same pułapki nadal obowiązują. Nadal potrzebujesz jasnych konwencji dla scope'ów, anulowania, limitów retry i zasad wątków.
Szybka lista kontrolna: Coroutines, RxJava czy oba?
Zacznij od kształtu pracy. Większość wywołań sieciowych to jednorazowe żądania, ale aplikacje mają też sygnały trwające w czasie, jak stan połączenia, auth czy live updates. Wybranie złej abstrakcji wcześnie zwykle objawia się później jako poplątane anulowania i trudne do czytania ścieżki błędów.
Prosty sposób decyzji (i wyjaśnienia zespołowi):
- Jednorazowe żądanie (logowanie, pobierz profil): preferuj funkcję
suspend. - Trwały strumień (zdarzenia, aktualizacje bazy): preferuj
Flowlub RxObservable. - Anulowanie związane z lifecycle UI: korutyny w
viewModelScopelublifecycleScopesą często prostsze niż ręczne Disposables. - Duże poleganie na zaawansowanych operatorach strumieni i backpressure: RxJava może nadal być lepszym wyborem, zwłaszcza w starszych bazach kodu.
- Złożone retry i mapowanie błędów: wybierz podejście, które zespół potrafi utrzymać czytelne.
Praktyczna reguła: jeśli na ekranie wykonujesz jedno żądanie i renderujesz wynik, korutyny trzymają kod blisko zwykłej funkcji. Jeśli budujesz pipeline wielu zdarzeń (pisanie, debounce, anuluj poprzednie żądania, łączenie filtrów), RxJava lub Flow często będą wygodniejsze.
Spójność bije perfekcję. Dwa dobre wzorce stosowane wszędzie są łatwiejsze w utrzymaniu niż pięć „najlepszych” wzorców używanych niekonsekwentnie.
Przykładowy scenariusz: logowanie, pobranie profilu i synchronizacja w tle
Typowy produkcyjny przepływ: użytkownik klika Logowanie, wywołujesz endpoint auth, potem pobierasz profil dla ekranu głównego i w końcu uruchamiasz synchronizację w tle. Tutaj Kotlin Coroutines vs RxJava może się różnić w codziennej konserwacji.
Wersja z korutynami (sekwencyjnie + anulowalna)
W korutynach kształt „zrób to, potem tamto” jest naturalny. Jeśli użytkownik zamknie ekran, anulowanie scope'a zatrzyma prace w locie.
suspend fun loginAndLoadProfile(): Result<Profile> = runCatching {
val token = api.login(email, password) // suspend
val profile = api.profile("Bearer $token")
syncManager.startSyncInBackground(token) // fire-and-forget
profile
}.recoverCatching { e ->
throw when (e) {
is HttpException -> when (e.code()) {
401 -> AuthExpiredException()
in 500..599 -> ServerDownException()
else -> e
}
is IOException -> NoNetworkException()
else -> e
}
}
// UI layer
val job = viewModelScope.launch { loginAndLoadProfile() }
override fun onCleared() { job.cancel() }
Wersja RxJava (łańcuch + disposal)
W RxJava ten sam przepływ to łańcuch. Anulowanie oznacza dispose, zwykle w CompositeDisposable.
val d = api.login(email, password)
.flatMap { token -> api.profile("Bearer $token").map { it to token } }
.doOnSuccess { (_, token) -> syncManager.startSyncInBackground(token) }
.onErrorResumeNext { e: Throwable ->
Single.error(
when (e) {
is HttpException -> if (e.code() == 401) AuthExpiredException() else e
is IOException -> NoNetworkException()
else -> e
}
)
}
.subscribe({ (profile, _) -> show(profile) }, { showError(it) })
compositeDisposable.add(d)
override fun onCleared() { compositeDisposable.clear() }
Minimalny zestaw testów powinien pokryć trzy wyniki: sukces, zmapowane porażki (401, 500, brak sieci) oraz anulowanie/dispose.
Następne kroki: ustal konwencje i trzymaj się ich
Zespoły zwykle wpadają w problemy, bo wzorce różnią się między funkcjami, nie dlatego, że Kotlin Coroutines vs RxJava jest „zły”. Krótkie notatki decyzyjne (nawet jedna strona) oszczędzają czas w review i czynią zachowanie przewidywalnym.
Zacznij od jasnego podziału: praca jednorazowa (pojedyncze wywołanie, które zwraca raz) vs strumienie (aktualizacje w czasie, jak websockety, lokalizacja, zmiany bazy). Zdecyduj domyślnie dla każdego i określ, kiedy dopuszczalne są wyjątki.
Następnie dodaj zestaw wspólnych helperów, żeby każda funkcja zachowywała się podobnie przy problemach sieciowych:
- Jedno miejsce do mapowania błędów (kody HTTP, timeouty, offline) na błędy na poziomie aplikacji, które UI rozumie
- Domyślne wartości timeout dla wywołań sieciowych z możliwością nadpisania dla długich operacji
- Polityka retry określająca, co jest bezpieczne do ponawiania (np. GET vs POST)
- Zasada anulowania: co się zatrzymuje po opuszczeniu ekranu, a co może kontynuować
- Zasady logowania pomagające wsparciu bez wycieków danych wrażliwych
Konwencje testowe są równie ważne. Uzgodnij standardowe podejście, by testy nie zależały od rzeczywistego czasu ani wątków. Dla korutyn zwykle oznacza to test dispatcher i ustrukturyzowane scope'y. Dla RxJava zwykle oznacza to test schedulery i jawne dispose. W obu przypadkach dąż do szybkich, deterministycznych testów bez sleepów.
Jeśli chcesz przyspieszyć cały proces, AppMaster (appmaster.io) jest jedną z opcji do generowania API backendowego i Kotlin‑owych mobilnych aplikacji bez pisania wszystkiego ręcznie. Nawet przy kodzie generowanym te same produkcyjne konwencje wokół anulowania, błędów, retry i testów są tym, co utrzymuje przewidywalne zachowanie sieciowe.
FAQ
Domyślnie wybieraj suspend dla pojedynczego wywołania zwracającego raz (np. logowanie, pobranie profilu). Używaj Flow (lub strumieni Rx), gdy wartości zmieniają się w czasie, np. wiadomości websocket, stan połączenia czy aktualizacje bazy danych.
Tak, ale tylko jeśli twój klient HTTP obsługuje anulowanie. Coroutines zatrzymują korutynę, gdy zakres zostanie anulowany, ale samo wywołanie HTTP również musi wspierać anulowanie, inaczej może kontynuować w tle.
Powiąż pracę z zakresem lifecycle, np. viewModelScope, aby została anulowana, gdy logika ekranu się kończy. Unikaj uruchamiania w długowiecznych lub globalnych zakresach, chyba że praca rzeczywiście powinna trwać dla całej aplikacji.
W korutynach błędy zwykle są zgłaszane przez wyjątki i obsługujesz je try/catch blisko miejsca, gdzie możesz zamapować na stan UI. W RxJava błędy płyną strumieniem — trzymaj ścieżkę błędów świadomie i unikaj operatorów, które cicho zamieniają błędy na wartości domyślne.
Używaj wyjątków dla nieoczekiwanych awarii jak timeouty, błędy 500 czy problemy z parsowaniem. Modeluj błędy jako typowane dane, gdy UI potrzebuje konkretnej informacji (np. „złe hasło” czy „adres e‑mail już zajęty”), żeby nie polegać na dopasowywaniu stringów.
Nałóż timeout tam, gdzie możesz pokazać odpowiedni komunikat UI, blisko miejsca wywołania. W korutynach withTimeout jest prosty dla suspend-calli; w RxJava operator timeout włącza timeout w pipeline.
Retry tylko gdy jest to bezpieczne — zwykle dla idempotentnych żądań (GET). Ogranicz liczbę prób (np. 2–3), nie retryuj przy błędach walidacji czy auth, i dodaj opóźnienie lub jitter, żeby nie zasypać serwera.
Główna pułapka to robienie ciężkiej pracy na wątku głównym. Coroutines używają Dispatchers (Main, IO, Default). RxJava ma subscribeOn (gdzie źródło działa) i observeOn (gdzie konsumujesz wyniki); trzymaj jeden finalny observeOn(main) tuż przed renderowaniem, żeby uniknąć niespodzianek.
Tak, ale trzymaj granicę małą i świadomą anulowania. Owiń Rx w suspend przy pomocy adaptera anulującego subskrypcję, gdy korutyna zostanie anulowana, i udostępniaj suspendowane API Rx callerom tylko w ograniczonych, dobrze udokumentowanych miejscach.
Używaj wirtualnego czasu, by testy nie zależały od rzeczywistych opóźnień. Dla korutyn runTest z test dispatcherem pozwala kontrolować delay i anulowania; dla RxJava używaj TestScheduler i sprawdzaj, że po dispose() nie ma dalszych emisji.


