Kotlin Coroutines vs RxJava pour le réseau et le travail en arrière‑plan
Kotlin Coroutines vs RxJava : comparez l'annulation, la gestion des erreurs et les patterns de test pour le réseau et le travail en arrière‑plan dans de vraies apps Android.

Pourquoi ce choix compte pour le réseau en production
Le réseau et le travail en arrière‑plan dans une vraie application Android, ce n'est pas juste un appel d'API. Il y a la connexion et le rafraîchissement de token, des écrans qui peuvent tourner en plein chargement, la synchronisation après que l'utilisateur quitte un écran, les uploads de photos, et des travaux périodiques qui ne doivent pas vider la batterie.
Les bugs les plus problématiques ne sont généralement pas des erreurs de syntaxe. Ils apparaissent quand un travail asynchrone survit à l'UI (fuites), quand l'annulation stoppe l'UI mais pas la requête réelle (trafic gaspillé et spinners bloqués), quand des retries multiplient les requêtes (limites de taux, bans), ou quand différentes couches traitent les erreurs différemment si bien que personne ne sait ce que verra l'utilisateur.
Le choix entre Kotlin Coroutines et RxJava affecte la fiabilité quotidienne :
- Comment vous modélisez le travail (appels one‑shot vs flux)
- Comment l'annulation se propage
- Comment les erreurs sont représentées et remontées à l'UI
- Comment vous contrôlez les threads pour réseau, disque, et UI
- Comment tester la temporisation, les retries et les cas limites
Les patterns ci‑dessous se concentrent sur ce qui casse sous charge ou sur des réseaux lents : annulation, gestion des erreurs, retries et timeouts, et habitudes de test qui évitent les régressions. Les exemples restent courts et pratiques.
Modèles mentaux principaux : appels suspendus, streams et Flow
La différence principale entre Kotlin Coroutines et RxJava est la forme du travail que vous modélisez.
Une fonction suspend représente une opération one‑shot. Elle renvoie une valeur ou lève une exception. Cela correspond à la plupart des appels réseau : récupérer un profil, mettre à jour des paramètres, uploader une photo. Le code appelant se lit de haut en bas, ce qui reste facile à parcourir même après avoir ajouté du logging, du caching et des branches.
RxJava commence par demander si vous traitez une valeur unique ou plusieurs valeurs dans le temps. Un Single est un résultat one‑shot (succès ou erreur). Un Observable (ou Flowable) est un flux qui peut émettre plusieurs valeurs, puis compléter ou échouer. Cela convient aux fonctionnalités véritablement événementielles : changements de texte, messages websocket, ou polling.
Flow est la manière adaptée aux coroutines pour représenter un flux. Pensez‑le comme la version « flux » des coroutines, avec une annulation structurée et une intégration directe aux API suspendues.
Règle rapide :
- Utilisez
suspendpour une requête et une réponse uniques. - Utilisez
Flowpour des valeurs qui changent dans le temps. - Utilisez RxJava quand votre app dépend déjà fortement des opérateurs et de la composition de flux complexe.
À mesure que les fonctionnalités grandissent, la lisibilité casse souvent quand vous forcez un modèle de flux sur un appel one‑shot, ou quand vous traitez des événements continus comme une seule valeur de retour. Adaptez l'abstraction à la réalité d'abord, puis définissez des conventions autour.
Annulation en pratique (avec courts exemples de code)
L'annulation est l'endroit où le code asynchrone semble sûr, ou où il provoque des crashes aléatoires et des appels gaspillés. L'objectif est simple : quand l'utilisateur quitte un écran, tout travail démarré pour cet écran doit s'arrêter.
Avec Kotlin Coroutines, l'annulation est intégrée au modèle. Un Job représente le travail, et avec la concurrence structurée vous ne passez généralement pas les jobs autour. Vous démarrez le travail dans une scope (comme viewModelScope). Quand cette scope est annulée, tout ce qui est à l'intérieur est aussi annulé.
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
}
}
Deux détails de production importent :
- Appelez le réseau via un client annulable. Sinon, la coroutine s'arrête mais l'appel HTTP peut continuer.
- Utilisez
withTimeout(ouwithTimeoutOrNull) pour les requêtes qui ne doivent pas rester bloquées.
RxJava utilise l'élimination explicite. Vous conservez un Disposable pour chaque subscription, ou les collectez dans un CompositeDisposable. Quand l'écran disparaît, vous disposez, et la chaîne doit s'arrêter.
class ProfilePresenter(private val api: ApiRx) {
private val bag = CompositeDisposable()
fun attach() {
bag += api.getProfile()
.subscribe(
{ profile -\u003e /* render */ },
{ error -\u003e /* show error */ }
)
}
fun detach() {
bag.clear() // cancels in-flight work if upstream supports cancellation
}
}
Une règle pratique en cas de sortie d'écran : si vous ne pouvez pas pointer où l'annulation se produit (annulation de scope ou dispose()), supposez que le travail va continuer et corrigez‑le avant la mise en production.
Gestion des erreurs lisible
Une grande différence entre Kotlin Coroutines et RxJava est la façon dont les erreurs voyagent. Les coroutines rendent les échecs semblables à du code normal : un appel suspend lève, et l'appelant décide quoi faire. Rx pousse les échecs dans le flux, ce qui est puissant, mais il est facile de masquer des problèmes si vous n'êtes pas vigilant.
Utilisez des exceptions pour les échecs inattendus (timeouts, 500, bugs de parsing). Modélisez les erreurs comme des données quand l'UI a besoin d'une réponse spécifique (mauvais mot de passe, « e‑mail déjà utilisé ») et que vous voulez que cela fasse partie de votre modèle de domaine.
Un simple pattern coroutine garde la stack trace et reste lisible :
suspend fun loadProfile(): Profile = try {
api.getProfile() // may throw
} catch (e: IOException) {
throw NetworkException("No connection", e)
}
runCatching et Result sont utiles quand vous voulez réellement renvoyer succès ou échec sans lever :
suspend fun loadProfileResult(): Result<Profile> =
runCatching { api.getProfile() }
Faites attention à getOrNull() si vous ne gérez pas aussi l'échec. Cela peut transformer silencieusement de vrais bugs en écrans « état vide ».
Avec RxJava, gardez le chemin d'erreur explicite. Utilisez onErrorReturn uniquement pour des fallback sûrs. Préférez onErrorResumeNext quand vous devez basculer de source (par ex. vers des données en cache). Pour les retries, restreignez les règles avec retryWhen pour ne pas relancer en cas de « mauvais mot de passe ».
Quelques habitudes pour éviter les erreurs avalées :
- Logger ou reporter une erreur une fois, proche du contexte où vous pouvez la corriger.
- Préserver l'exception originale comme
causelors d'un wrap. - Éviter les catch‑all qui transforment toutes les erreurs en une valeur par défaut.
- Faire des erreurs visibles par l'utilisateur un modèle typé, pas une chaîne.
Notions de threading : Dispatchers vs Schedulers
Beaucoup de bugs asynchrones viennent du threading : faire du travail lourd sur le thread principal, ou toucher l'UI depuis un thread de fond. Kotlin Coroutines et RxJava diffèrent surtout dans la façon d'exprimer les changements de thread.
Avec les coroutines, vous démarrez souvent sur le thread principal pour le travail UI, puis basculez sur un dispatcher background pour les parties coûteuses. Choix courants :
Dispatchers.Mainpour les mises à jour UIDispatchers.IOpour les I/O bloquants comme le réseau et le disqueDispatchers.Defaultpour le travail CPU (parsing JSON, tri, chiffrement)
Un pattern simple : récupérer les données, parser hors du main, puis rendre.
viewModelScope.launch(Dispatchers.Main) {
val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
val profile = withContext(Dispatchers.Default) { parseProfile(json) }
_uiState.value = UiState.Content(profile)
}
RxJava exprime « où le travail se produit » avec subscribeOn et « où les résultats sont observés » avec observeOn. Une surprise commune est de croire que observeOn affecte le travail en amont. Ce n'est pas le cas. subscribeOn définit le thread pour la source et les opérateurs au‑dessus, et chaque observeOn change le thread à partir de ce point.
api.fetchProfileJson()
.subscribeOn(Schedulers.io())
.map { json -\u003e parseProfile(json) } // still on io unless you change it
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ profile -\u003e render(profile) },
{ error -\u003e showError(error) }
)
Une règle pour éviter les surprises : centralisez le travail UI. En coroutines, assignez ou collectez l'état UI sur Dispatchers.Main. En RxJava, placez un observeOn(main) final juste avant le rendu, et n'éparpillez pas les observeOn sauf si nécessaire.
Si un écran saccade, retirez d'abord le parsing et le mapping du thread principal. Ce seul changement résout beaucoup de problèmes réels.
Timeouts, retries et travail parallèle pour les appels réseau
Le chemin heureux n'est rarement le problème. Les soucis viennent des appels qui se bloquent, des retries qui aggravent la situation, ou du travail « parallèle » qui n'est en fait pas parallèle. Ces patterns déterminent souvent la préférence entre Kotlin Coroutines et RxJava.
Timeouts qui échouent vite
Avec les coroutines, vous pouvez poser une limite autour de n'importe quel appel suspendu. Gardez le timeout proche du point d'appel pour afficher le bon message UI.
val user = withTimeout(5_000) {
api.getUser() // suspend
}
En RxJava, vous attachez un opérateur timeout au flux. Utile quand le comportement de timeout fait partie d'un pipeline partagé.
Retries sans dégâts
Retryez seulement quand c'est sûr. Règle simple : retryez plus librement les requêtes idempotentes (GET) que celles qui ont des effets de bord (création de commande). Même là, plafonnez le nombre, et ajoutez délai ou jitter.
Bonnes gardes par défaut :
- Retryz sur timeouts réseau et erreurs serveur temporaires.
- Ne retryz pas sur erreurs de validation (400) ou échecs d'auth.
- Plafonnez les retries (souvent 2–3) et loggez l'échec final.
- Utilisez des backoffs pour ne pas marteler le serveur.
En RxJava, retryWhen permet d'exprimer « retry seulement pour ces erreurs, avec ce délai ». En coroutines, Flow a retry et retryWhen, tandis que les fonctions suspend pures utilisent souvent une petite boucle avec des delay.
Appels parallèles sans code emmêlé
Les coroutines rendent le parallélisme direct : démarrez deux requêtes, attendez les deux.
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
RxJava brille quand combiner plusieurs sources est l'objet principal de la chaîne. zip est l'outil habituel pour « attendre les deux », et merge est utile quand vous voulez les résultats dès qu'ils arrivent.
Pour de grands flux ou des flux rapides, la pression en retour (backpressure) compte toujours. Le Flowable de RxJava possède des outils matures pour ça. Flow dans les coroutines gère beaucoup de cas, mais vous devrez parfois ajouter des politiques de buffering ou de dropping si les événements débordent votre UI ou vos écritures en base.
Interop et stratégies de migration (bases de code mixtes)
La plupart des équipes ne switchent pas du jour au lendemain. Une migration pratique conserve l'app stable tout en migrant module par module.
Envelopper une API Rx en une fonction suspendue
Si vous avez un Single<T> ou un Completable, enveloppez‑le avec support d'annulation pour que la coroutine annulée dispose la subscription Rx.
suspend fun <T : Any> Single<T>.awaitCancellable(): T =
suspendCancellableCoroutine { cont -\u003e
val d = subscribe(
{ value -\u003e cont.resume(value) {} },
{ error -\u003e cont.resumeWithException(error) }
)
cont.invokeOnCancellation { d.dispose() }
}
Cela évite un mode d'échec courant : l'utilisateur quitte l'écran, la coroutine est annulée, mais l'appel réseau continue et met à jour un état partagé plus tard.
Exposer du code coroutine à des appelants Rx
Pendant la migration, certaines couches attendront encore des types Rx. Enveloppez le travail suspendu dans Single.fromCallable et bloquez uniquement sur un thread background.
fun loadProfileRx(api: Api): Single<Profile> =
Single.fromCallable {
runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
}
Gardez cette frontière petite et documentée. Pour le code neuf, préférez appeler l'API suspendue directement depuis une scope coroutine.
Où Flow convient, et où pas
Flow peut remplacer beaucoup de cas d'Observable : état UI, mises à jour de base, et streams de paging. Il est moins direct si vous dépendez fortement de flux chauds (hot streams), de sujets (subjects), d'un tuning avancé du backpressure, ou d'un grand ensemble d'opérateurs personnalisés que votre équipe maîtrise déjà.
Stratégie de migration pour limiter la confusion :
- Convertissez d'abord les modules feuille (réseau, stockage) en API suspendues.
- Ajoutez de petits adaptateurs aux frontières de module (Rx -> suspend, suspend -> Rx).
- Remplacez les streams Rx par Flow seulement quand vous contrôlez aussi les consommateurs.
- Gardez un style asynchrone par zone fonctionnelle.
- Supprimez les adaptateurs dès que le dernier appelant est migré.
Patterns de test que vous utiliserez vraiment
Les problèmes de temporisation et d'annulation sont là où les bugs asynchrones se cachent. De bons tests asynchrones rendent le temps déterministe et les résultats faciles à asserter. C'est un autre domaine où Kotlin Coroutines et RxJava donnent des expériences différentes, même si les deux peuvent être bien testés.
Coroutines : runTest, TestDispatcher et contrôle du temps
Pour le code coroutine, préférez runTest avec un test dispatcher pour que votre test ne dépende ni de threads réels ni de vrais délais. Le temps virtuel permet de déclencher timeouts, retries et fenêtres de debounce sans dormir.
@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()
}
Pour tester l'annulation, annulez le Job collectant (ou la scope parent) et vérifiez que votre fake API s'arrête ou qu'aucun autre état n'est émis.
RxJava : TestScheduler, TestObserver, temps déterministe
Les tests Rx combinent généralement un TestScheduler pour le temps et un TestObserver pour les assertions.
@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
}
Quand vous testez des chemins d'erreur dans l'un ou l'autre style, concentrez‑vous sur le mapping, pas sur le type d'exception. Vérifiez l'état UI attendu après un 401, un timeout, ou une réponse mal formée.
Un petit ensemble de vérifications couvre la plupart des régressions :
- États Loading et finaux (Success, Empty, Error)
- Nettoyage d'annulation (job annulé, disposable disposé)
- Mapping d'erreurs (codes serveur -> messages utilisateur)
- Pas d'émissions dupliquées après retries
- Logique temporelle testée avec temps virtuel, pas avec de vrais délais
Erreurs communes qui causent des bugs en production
La plupart des problèmes en production ne viennent pas du choix Kotlin Coroutines vs RxJava. Ils proviennent de quelques habitudes qui font que le travail dure plus longtemps que prévu, s'exécute deux fois, ou touche l'UI au mauvais moment.
Une fuite commune est de lancer du travail dans la mauvaise scope. Si vous démarrez un appel réseau depuis une scope qui vit plus longtemps que l'écran (ou que vous créez votre propre scope et ne l'annulez jamais), la requête peut se terminer après que l'utilisateur est parti et tenter de mettre à jour l'état. En coroutines, cela ressemble souvent à l'utilisation par défaut d'une scope longue durée. En RxJava, c'est habituellement un dispose oublié.
Un autre classique est le « fire and forget ». Les scopes globales et les Disposables oubliés semblent fonctionner jusqu'à ce que le travail s'accumule. Un écran de chat qui refresh à chaque resume peut vite accumuler plusieurs jobs de refresh après quelques navigations, chacun consommant de la mémoire et du réseau.
Les retries sont aussi faciles à mal implémenter. Des retries illimités ou sans délai peuvent spammer votre backend et vider la batterie. C'est particulièrement dangereux pour des échecs permanents comme un 401 après logout. Rendez les retries conditionnels, ajoutez du backoff, et arrêtez quand l'erreur n'est pas récupérable.
Les erreurs de threading provoquent des crashes difficiles à reproduire. Vous pouvez parser du JSON sur le main thread ou mettre à jour l'UI depuis un thread de fond selon l'endroit où vous placez un dispatcher ou scheduler.
Vérifications rapides qui attrapent la plupart de ces problèmes :
- Liez le travail à un propriétaire de cycle de vie et annulez‑le quand il se termine.
- Rendre le nettoyage évident : annulez les Jobs ou clear les Disposables en un seul endroit.
- Imposer des limites strictes sur les retries (nombre, délai, erreurs éligibles).
- Faire respecter une règle : mises à jour UI uniquement sur le main thread lors des revues de code.
- Traitez la synchro en arrière‑plan comme un système avec contraintes, pas comme un simple appel de fonction.
Si vous livrez des apps Android à partir de code Kotlin généré (par exemple avec AppMaster), les mêmes pièges s'appliquent. Vous avez toujours besoin de conventions claires pour les scopes, l'annulation, les limites de retry et les règles de threads.
Checklist rapide pour choisir Coroutines, RxJava, ou les deux
Commencez par la forme du travail. La plupart des appels réseau sont one‑shot, mais les apps ont aussi des signaux continus comme la connectivité, l'état d'auth, ou des mises à jour en direct. Choisir la mauvaise abstraction tôt se traduira plus tard par une annulation compliquée et des chemins d'erreur difficiles à lire.
Une façon simple de décider (et de l'expliquer à votre équipe) :
- Requête unique (login, fetch profile) : préférez une fonction
suspend. - Flux continu (événements, mises à jour DB) : préférez
FlowouObservableRx. - Annulation liée au cycle de vie : coroutines dans
viewModelScopeoulifecycleScopesont souvent plus simples que des disposables manuels. - Forte dépendance aux opérateurs avancés et au backpressure : RxJava peut rester mieux adapté, surtout dans les bases de code plus anciennes.
- Retries et mapping d'erreurs complexes : choisissez l'approche que votre équipe peut garder lisible.
Règle pratique : si un écran fait une requête unique et rend un résultat, les coroutines gardent le code proche d'un appel de fonction normal. Si vous construisez un pipeline d'événements (typing, debounce, annuler la précédente requête, combiner filtres), RxJava ou Flow peut sembler plus naturel.
La cohérence bat la perfection. Deux bons patterns utilisés partout sont plus faciles à maintenir que cinq « meilleurs » patterns employés de façon incohérente.
Scénario exemple : login, fetch profil et sync en arrière‑plan
Un flux courant en production : l'utilisateur appuie sur Login, vous appelez l'endpoint d'auth, puis récupérez le profil pour l'écran d'accueil, et enfin lancez une synchro en arrière‑plan. C'est là que Kotlin Coroutines vs RxJava peut peser au quotidien.
Version Coroutines (séquentielle + annulable)
Avec les coroutines, le flux « faire ceci, puis cela » est naturel. Si l'utilisateur ferme l'écran, l'annulation de la scope stoppe le travail en cours.
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() }
Version RxJava (chaîne + disposal)
En RxJava, le même flux s'écrit en chaîne. L'annulation signifie disposer, en général via un 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() }
Une suite de tests minimale ici devrait couvrir trois issues : succès, échecs mappés (401, 500, pas de réseau), et annulation/disposal.
Étapes suivantes : définissez des conventions et appliquez‑les
Les équipes ont généralement des problèmes parce que les patterns varient entre fonctionnalités, pas parce que Kotlin Coroutines vs RxJava est intrinsèquement « mauvais ». Une courte note de décision (même une page) économise du temps en revue et rend le comportement prévisible.
Commencez par une séparation claire : travail one‑shot vs flux continus. Décidez du défaut pour chaque cas, et définissez quand des exceptions sont autorisées.
Ajoutez ensuite un petit set d'aides partagées pour que chaque fonctionnalité se comporte de la même manière quand le réseau se dégrade :
- Un endroit pour mapper les erreurs (codes HTTP, timeouts, offline) en échecs au niveau app que l'UI comprend
- Valeurs par défaut de timeout pour les appels réseau, avec une façon claire d'outrepasser pour les opérations longues
- Une politique de retry qui spécifie ce qu'il est sûr de relancer (par ex. GET vs POST)
- Une règle d'annulation : ce qui s'arrête quand l'utilisateur quitte et ce qui peut continuer
- Règles de logging qui aident le support sans exposer de données sensibles
Les conventions de tests comptent tout autant. Mettez‑vous d'accord sur une approche standard pour que les tests ne dépendent ni du temps réel ni de vrais threads. Pour les coroutines, cela signifie en général un test dispatcher et des scopes structurées. Pour RxJava, cela signifie des test schedulers et une élimination explicite. Dans tous les cas, visez des tests rapides et déterministes sans sleeps.
Si vous cherchez à accélérer globalement, AppMaster (appmaster.io) est une option pour générer des APIs backend et des apps Kotlin sans tout écrire à la main. Même avec du code généré, les mêmes conventions de production autour de l'annulation, des erreurs, des retries et des tests sont ce qui rend le comportement réseau prévisible.
FAQ
Par défaut, utilisez suspend pour une requête qui renvoie une seule fois, comme un login ou la récupération d'un profil. Utilisez Flow (ou des flux Rx) lorsque les valeurs évoluent dans le temps, comme les messages websocket, l'état de connectivité ou les mises à jour de la base de données.
Oui, mais seulement si votre client HTTP est annulable. Les coroutines annulent la coroutine lorsque la portée est annulée, mais l'appel HTTP sous-jacent doit aussi supporter l'annulation sinon la requête peut continuer en tâche de fond.
Rattachez le travail à une scope liée au cycle de vie, comme viewModelScope, afin qu'il soit annulé quand la logique d'écran se termine. Évitez de lancer depuis des scopes longue‑durée ou globaux sauf si le travail est véritablement global à l'application.
Avec les coroutines, les échecs se traduisent souvent par des exceptions et se gèrent avec try/catch près de l'appel pour les mapper à l'état UI. Avec RxJava, les erreurs circulent dans le flux : maintenez un chemin d'erreur explicite et évitez les opérateurs qui transforment silencieusement les échecs en valeurs par défaut.
Utilisez des exceptions pour les échecs inattendus (timeouts, 500, erreurs de parsing). Modélisez les erreurs sous forme de données typées quand l'UI doit réagir à des cas précis comme « mauvais mot de passe » ou « e-mail déjà utilisé », plutôt que de se baser sur des chaînes.
Appliquez le timeout là où vous pouvez afficher le bon message UI, c'est‑à‑dire près du point d'appel. En coroutines, withTimeout est simple pour les appels suspend; en RxJava, l'opérateur timeout intègre le timeout dans la chaîne.
Retryz seulement quand c'est sûr : typiquement pour les requêtes idempotentes comme GET, et limitez le nombre de tentatives (2–3). N'effectuez pas de retry sur des erreurs de validation ou d'authentification, et ajoutez des délais/jitter pour éviter de surcharger le serveur ou de vider la batterie.
Le pattern fréquent : démarrez sur Main pour l'UI, basculez vers IO/Default pour le travail coûteux, et assurez‑vous que les mises à jour UI se produisent uniquement sur le thread principal. En RxJava, mettez un observeOn(main) final juste avant le rendu pour éviter les surprises.
Oui. Vous pouvez migrer progressivement mais gardez les frontières petites et conscientes de l'annulation. Enveloppez du Rx en suspend avec un adaptateur annulable qui dispose la subscription quand la coroutine est annulée, et exposez le code suspend aux appels Rx seulement via des ponts bien documentés.
Contrôlez le temps via du temps virtuel pour que les tests ne fassent pas de sleeps. Pour les coroutines, runTest avec un test dispatcher permet de piloter délais et annulations ; pour RxJava, utilisez TestScheduler et vérifiez qu'il n'y a pas d'émissions après dispose().


