13 abr 2025·7 min de lectura

Kotlin Coroutines vs RxJava para networking y trabajo en segundo plano

Kotlin Coroutines vs RxJava: compara cancelación, manejo de errores y patrones de testing para networking y trabajo en background en apps Android reales.

Kotlin Coroutines vs RxJava para networking y trabajo en segundo plano

Por qué esta elección importa en networking de producción

El networking y el trabajo en segundo plano en una app Android real es más que una sola llamada a la API. Incluye login y refresco de token, pantallas que pueden rotarse mientras cargan, sincronización después de que el usuario abandona una pantalla, subidas de fotos y trabajo periódico que no puede agotar la batería.

Los bugs que más duelen normalmente no son problemas de sintaxis. Aparecen cuando el trabajo asíncrono vive más que la UI (leaks), cuando la cancelación para la UI pero no la petición real (tráfico desperdiciado y spinners atascados), cuando los reintentos multiplican peticiones (límites de tasa, baneo) o cuando diferentes capas manejan errores de formas distintas y nadie puede predecir qué verá el usuario.

La decisión entre Kotlin Coroutines vs RxJava afecta la fiabilidad diaria:

  • Cómo modelas el trabajo (llamadas one-shot vs streams)
  • Cómo se propaga la cancelación
  • Cómo se representan y exponen los errores en la UI
  • Cómo controlas los hilos para red, disco y UI
  • Qué tan testeables son tiempos, reintentos y casos límite

Los patrones que siguen se centran en lo que tiende a romperse bajo carga o en redes lentas: cancelación, manejo de errores, reintentos y timeouts, y hábitos de testing que previenen regresiones. Los ejemplos son cortos y prácticos.

Modelos mentales básicos: llamadas suspend, streams y Flow

La principal diferencia entre Kotlin Coroutines vs RxJava es la forma del trabajo que modelas.

Una función suspend representa una operación one-shot. Devuelve un valor o lanza una excepción. Eso coincide con la mayoría de llamadas de networking: obtener perfil, actualizar ajustes, subir una foto. El código que la llama se lee de arriba a abajo, lo que sigue siendo fácil de escanear incluso después de añadir logging, caché y bifurcaciones.

RxJava empieza preguntando si tratas con un valor o con muchos valores a lo largo del tiempo. Un Single es un resultado one-shot (éxito o error). Un Observable (o Flowable) es un stream que puede emitir muchos valores, luego completarse o fallar. Esto encaja con características que son realmente tipo evento: cambios de texto, mensajes de websocket o polling.

Flow es la forma amigable con corutinas de representar un stream. Piénsalo como la “versión stream” de las corutinas, con cancelación estructurada y encaje directo con APIs suspend.

Una regla rápida:

  • Usa suspend para una petición y una respuesta.
  • Usa Flow para valores que cambian con el tiempo.
  • Usa RxJava cuando tu app ya depende mucho de operadores y composición compleja de streams.

A medida que las features crecen, la legibilidad suele romperse primero cuando fuerzas un modelo de stream sobre una llamada one-shot, o tratas eventos continuos como un único valor de retorno. Empata la abstracción con la realidad primero y luego construye convenciones alrededor.

Cancelación en la práctica (con ejemplos cortos)

La cancelación es donde el código asíncrono o se siente seguro, o se convierte en crashes aleatorios y llamadas desperdiciadas. La meta es simple: cuando el usuario deja una pantalla, cualquier trabajo iniciado para esa pantalla debería detenerse.

Con Kotlin Coroutines, la cancelación está integrada en el modelo. Un Job representa trabajo, y con concurrencia estructurada normalmente no pasas jobs por ahí. Inicias trabajo dentro de un scope (como el scope del ViewModel). Cuando ese scope se cancela, todo dentro también se cancela.

class ProfileViewModel(
    private val api: Api
) : ViewModel() {

    fun loadProfile() = viewModelScope.launch {
        // Si el ViewModel se limpia, esta coroutine se cancela,
        // y también la llamada de red en vuelo (si el cliente la soporta).
        val profile = api.getProfile() // suspend
        // actualizar estado de UI aquí
    }
}

Dos detalles de producción importan:

  • Llama a networking suspend a través de un cliente cancelable. Si no, la coroutine se detiene pero la llamada HTTP puede seguir ejecutándose.
  • Usa withTimeout (o withTimeoutOrNull) para peticiones que no deben colgarse.

RxJava usa disposal explícito. Mantienes un Disposable por suscripción, o los recolectas en un CompositeDisposable. Cuando la pantalla desaparece, haces dispose, y la cadena debería detenerse.

class ProfilePresenter(private val api: ApiRx) {
    private val bag = CompositeDisposable()

    fun attach() {
        bag += api.getProfile()
            .subscribe(
                { profile -> /* render */ },
                { error -> /* mostrar error */ }
            )
    }

    fun detach() {
        bag.clear() // cancela trabajo en vuelo si el upstream soporta cancelación
    }
}

Una regla práctica para la salida de pantalla: si no puedes señalar dónde ocurre la cancelación (cancelación de scope o dispose()), asume que el trabajo seguirá ejecutándose y arréglalo antes de publicar.

Manejo de errores que siga siendo entendible

Una gran diferencia entre Kotlin Coroutines vs RxJava es cómo viajan los errores. Las corutinas hacen que las fallas se vean como código normal: una llamada suspend lanza y el llamador decide qué hacer. Rx empuja fallos a través del stream, lo cual es potente, pero es fácil ocultar problemas si no tienes cuidado.

Usa excepciones para fallos inesperados (timeouts, 500s, bugs de parseo). Modela errores como datos cuando la UI necesita una respuesta específica (contraseña incorrecta, “email ya usado”) y quieres que eso sea parte del modelo de dominio.

Un patrón sencillo en corutinas mantiene el stack trace y es legible:

suspend fun loadProfile(): Profile = try {
    api.getProfile() // puede lanzar
} catch (e: IOException) {
    throw NetworkException("Sin conexión", e)
}

runCatching y Result son útiles cuando realmente quieres devolver éxito o fallo sin lanzar:

suspend fun loadProfileResult(): Result<Profile> =
    runCatching { api.getProfile() }

Ten cuidado con getOrNull() si no manejas también el fallo. Eso puede convertir silenciosamente bugs reales en pantallas de “estado vacío”.

En RxJava, mantén la ruta de error explícita. Usa onErrorReturn solo para fallback seguros. Prefiere onErrorResumeNext cuando necesites cambiar de fuente (por ejemplo, a datos cacheados). Para reintentos, mantén reglas estrictas con retryWhen para no reintentar en casos como “contraseña incorrecta”.

Un conjunto de hábitos que evita errores tragados:

  • Registra o reporta un error una vez, cerca de donde tienes contexto.
  • Preserva la excepción original como cause cuando envuelves.
  • Evita fallbacks que conviertan todo error en un valor por defecto.
  • Haz que los errores mostrados al usuario sean un modelo tipado, no una cadena.

Conceptos básicos de hilos: Dispatchers vs Schedulers

Convierte patrones en una app
Construye una app Android de producción con código Kotlin generado y APIs de backend claras.
Probar AppMaster

Muchos bugs asíncronos se reducen a hilos: hacer trabajo pesado en el hilo principal, o tocar la UI desde un hilo de fondo. Kotlin Coroutines vs RxJava difieren principalmente en cómo expresas cambios de hilo.

Con corutinas, normalmente empiezas en el hilo principal para trabajo de UI y luego saltas a un dispatcher de fondo para las partes costosas. Opciones comunes:

  • Dispatchers.Main para actualizaciones de UI
  • Dispatchers.IO para I/O bloqueante como red y disco
  • Dispatchers.Default para trabajo CPU intensivo como parseo JSON, ordenado, encriptación

Un patrón sencillo: obtener datos, parsear fuera del main y luego renderizar.

viewModelScope.launch(Dispatchers.Main) {
    val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
    val profile = withContext(Dispatchers.Default) { parseProfile(json) }
    _uiState.value = UiState.Content(profile)
}

RxJava expresa “dónde corre el trabajo” con subscribeOn y “dónde se observan los resultados” con observeOn. Una sorpresa común es esperar que observeOn afecte el trabajo upstream. No lo hace. subscribeOn fija el hilo para la fuente y operadores por encima; cada observeOn cambia hilos desde ese punto en adelante.

api.fetchProfileJson()
    .subscribeOn(Schedulers.io())
    .map { json -> parseProfile(json) } // sigue en io a menos que lo cambies
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        { profile -> render(profile) },
        { error -> showError(error) }
    )

Una regla para evitar sorpresas: mantén el trabajo de UI en un solo lugar. En corutinas, asigna o colecta el estado de UI en Dispatchers.Main. En RxJava, pon un observeOn(main) final antes de renderizar y no esparzas observeOn a menos que realmente lo necesites.

Si la pantalla se queda lenta, mueve parseo y mapeo fuera del hilo principal primero. Ese simple cambio arregla muchos problemas del mundo real.

Reintentos, timeouts y trabajo paralelo para llamadas de red

Prueba flujos asíncronos antes
Prototipa flujos de login, perfil y sincronización con lógica visual en lugar de código frágil.
Probar

El camino feliz rara vez es el problema. Las cuestiones vienen de llamadas que cuelgan, reintentos que empeoran la situación, o trabajo “paralelo” que en realidad no es paralelo. Estos patrones a menudo deciden si un equipo prefiere Kotlin Coroutines vs RxJava.

Timeouts que fallen rápido

Con corutinas, puedes poner un tope duro alrededor de cualquier llamada suspend. Mantén el timeout cerca del sitio de la llamada para poder mostrar el mensaje correcto en la UI.

val user = withTimeout(5_000) {
    api.getUser() // suspend
}

En RxJava, adjuntas un operador timeout al stream. Eso es útil cuando el comportamiento de timeout debe ser parte de una pipeline compartida.

Reintentos sin causar daño

Reintenta solo cuando reintentar sea seguro. Una regla simple: reintenta peticiones idempotentes (como GET) con más libertad que peticiones que crean efectos secundarios (como “crear pedido”). Incluso entonces, limita la cantidad y añade delay o jitter.

Buenas guardas por defecto:

  • Reintentar en timeouts de red y errores temporales del servidor.
  • No reintentar en errores de validación (400s) o fallos de auth.
  • Limitar reintentos (a menudo 2–3) y loguear el fallo final.
  • Usar backoff con delays para no bombardear el servidor.

En RxJava, retryWhen te deja expresar “reintentar solo para estos errores, con este delay”. En corutinas, Flow tiene retry y retryWhen, mientras que funciones suspend a menudo usan un pequeño bucle con delay.

Llamadas paralelas sin código enredado

Las corutinas hacen el trabajo paralelo directo: inicia dos peticiones y espera ambas.

coroutineScope {
    val profile = async { api.getProfile() }
    val feed = async { api.getFeed() }
    profile.await() to feed.await()
}

RxJava destaca cuando combinar múltiples fuentes es el propósito de la cadena. zip es la herramienta típica para “esperar ambos”, y merge sirve cuando quieres resultados apenas lleguen.

Para streams grandes o rápidos, el backpressure sigue importando. Flowable de RxJava tiene herramientas maduras para backpressure. Flow de corutinas maneja muchos casos bien, pero aún puede necesitar buffering o políticas de descarte si los eventos superan la velocidad de la UI o de las escrituras a BD.

Interop y patrones de migración (codebases mixtas)

La mayoría de equipos no cambian de la noche a la mañana. Una migración práctica de Kotlin Coroutines vs RxJava mantiene la app estable mientras mueves módulo por módulo.

Envolver una API Rx en una función suspend

Si tienes un Single<T> o Completable existente, envuélvelo con soporte de cancelación para que una coroutine cancelada haga dispose de la suscripción 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() }
  }

Esto evita un modo de fallo común: el usuario sale de la pantalla, la coroutine se cancela, pero la llamada de red sigue y actualiza estado compartido más tarde.

Exponer código coroutine a llamadores Rx

Durante la migración, algunas capas seguirán esperando tipos Rx. Envuelve trabajo suspend en Single.fromCallable y bloquea solo en un hilo de fondo.

fun loadProfileRx(api: Api): Single<Profile> =
  Single.fromCallable {
    runBlocking { api.loadProfile() } // asegurar subscribeOn(Schedulers.io())
  }

Mantén este límite pequeño y documentado. Para código nuevo, prefiere llamar la API suspend directamente desde un coroutine scope.

Dónde encaja Flow y dónde no

Flow puede reemplazar muchos casos de Observable: estado de UI, actualizaciones de BD y streams tipo paging. Puede ser menos directo si dependes mucho de streams hot, subjects, ajuste avanzado de backpressure o de un gran conjunto de operadores personalizados que tu equipo ya domina.

Una estrategia de migración que reduzca confusión:

  • Convierte módulos hoja primero (network, almacenamiento) a APIs suspend.
  • Añade pequeños adaptadores en los límites de módulo (Rx a suspend, suspend a Rx).
  • Reemplaza streams Rx por Flow solo cuando controles también a los consumidores.
  • Mantén un estilo async por área de feature.
  • Borra adaptadores tan pronto como el último llamador migre.

Patrones de testing que realmente usarás

Evita la deuda técnica desde temprano
Obtén código fuente real que puedas revisar, extender y mantener conforme cambian los requisitos.
Generar código

Los problemas de timing y cancelación son donde se esconden los bugs asíncronos. Buenos tests asíncronos hacen el tiempo determinista y los resultados fáciles de afirmar. Aquí también Kotlin Coroutines vs RxJava se siente distinto, aunque ambos se pueden testear bien.

Corutinas: runTest, TestDispatcher y control del tiempo

Para código con corutinas, prefiere runTest con un test dispatcher para que tu test no dependa de hilos reales o delays reales. El tiempo virtual te permite disparar timeouts, reintentos y ventanas de debounce sin dormir.

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `emite Loading luego 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() // ejecuta trabajo encolado
  assert(states.first() is UiState.Loading)

  testScheduler.advanceTimeBy(1_000) // dispara delays/retry
  testScheduler.runCurrent()
  assert(states.last() is UiState.Success)

  job.cancel()
}

Para probar cancelación, cancela el Job que colecta (o el scope padre) y afirma que tu API falsa se detiene o que no hay más emisiones.

RxJava: TestScheduler, TestObserver, tiempo determinista

Los tests en Rx suelen combinar un TestScheduler para tiempo y un TestObserver para aserciones.

@Test
fun `dispose en cancel y se detienen emisiones`() {
  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 más eventos después de dispose
}

Al probar rutas de error en cualquiera de los dos estilos, céntrate en el mapeo, no en el tipo de excepción. Asegura el estado de UI que esperas tras un 401, un timeout o una respuesta malformada.

Un pequeño conjunto de comprobaciones cubre la mayoría de regresiones:

  • Estados Loading y finales (Success, Empty, Error)
  • Limpieza de cancelación (job cancelado, disposable disposed)
  • Mapeo de errores (códigos HTTP a mensajes de usuario)
  • No duplicar emisiones tras reintentos
  • Lógica basada en tiempo usando tiempo virtual, no sleeps reales

Errores comunes que causan bugs en producción

La mayoría de problemas de producción no se deben a elegir Kotlin Coroutines vs RxJava. Provienen de unos pocos hábitos que hacen que el trabajo se ejecute más tiempo del previsto, se ejecute dos veces o toque la UI en el momento equivocado.

Un leak común es lanzar trabajo en el scope equivocado. Si inicias una llamada de red desde un scope que vive más que la pantalla (o creas tu propio scope y nunca lo cancelas), la petición puede terminar después de que el usuario se vaya e intentar actualizar estado. En corutinas, esto suele verse al usar un scope de larga duración por defecto. En RxJava, normalmente es un dispose() olvidado.

Otro clásico es “fire and forget”. Scopes globales y Disposables olvidados parecen funcionar hasta que el trabajo se acumula. Una pantalla de chat que refresca en cada resume puede acabar con múltiples jobs de refresh tras unas navegaciones, cada uno reteniendo memoria y compitiendo por la red.

Los reintentos también son fáciles de hacer mal. Reintentos ilimitados o sin delays pueden spamear tu backend y drenar batería. Es especialmente peligroso cuando el fallo es permanente, como un 401 tras logout. Haz los reintentos condicionales, añade backoff y para cuando el error no sea recuperable.

Los errores de hilos causan crashes difíciles de reproducir. Podrías parsear JSON en el main thread o actualizar la UI desde un hilo de fondo según dónde pusiste un dispatcher o scheduler.

Comprobaciones rápidas que atrapan la mayoría de estos problemas:

  • Ata el trabajo a un lifecycle owner y cancélalo cuando ese owner termine.
  • Haz la limpieza obvia: cancela Jobs o limpia Disposables en un solo lugar.
  • Pon límites estrictos en reintentos (conteo, delay y qué errores califican).
  • Haz cumplir una regla para actualizaciones de UI (solo main thread) en las revisiones de código.
  • Trata la sincronización en background como un sistema con restricciones, no como una llamada de función aleatoria.

Si publicas apps Android desde código Kotlin generado (por ejemplo, desde AppMaster), las mismas trampas siguen aplicando. Siguen necesarias convenciones claras para scopes, cancelación, límites de reintento y reglas de hilos.

Lista rápida para elegir Coroutines, RxJava o ambos

Haz la lógica predecible
Crea lógica de negocio con workflows drag-and-drop que reflejen cómo se comporta tu app.
Construir sin código

Comienza con la forma del trabajo. La mayoría de llamadas de networking son one-shot, pero las apps también tienen señales continuas como conectividad, estado de auth o actualizaciones en vivo. Elegir la abstracción equivocada temprano suele aparecer después como cancelación desordenada y rutas de error difíciles de leer.

Una forma simple de decidir (y explicarlo al equipo):

  • Petición única (login, obtener perfil): prefiere una función suspend.
  • Stream continuo (eventos, actualizaciones DB): prefiere Flow u Observable de Rx.
  • Cancelación ligada al ciclo de vida: corutinas en viewModelScope o lifecycleScope suelen ser más simples que disposables manuales.
  • Gran dependencia de operadores avanzados y backpressure: RxJava puede seguir siendo mejor, sobre todo en codebases antiguas.
  • Reintentos complejos y mapeo de errores: elige el enfoque que tu equipo pueda mantener legible.

Una regla práctica: si una pantalla hace una solicitud y renderiza un resultado, las corutinas mantienen el código parecido a una función normal. Si construyes una pipeline de muchos eventos (typing, debounce, cancelar peticiones previas, combinar filtros), RxJava o Flow suelen sentirse más naturales.

La consistencia vence a la perfección. Dos buenos patrones usados en todas partes son más fáciles de mantener que cinco “mejores” patrones usados de forma inconsistente.

Escenario de ejemplo: login, obtención de perfil y sync en background

Crea tu backend rápidamente
Modela datos, añade autenticación y publica endpoints sin escribir el esqueleto del servidor a mano.
Comenzar a construir

Un flujo común de producción es: el usuario pulsa Login, llamas a un endpoint de auth, luego obtienes el perfil para la pantalla principal y finalmente arrancas una sincronización en background. Aquí es donde Kotlin Coroutines vs RxJava puede sentirse distinto en el mantenimiento diario.

Versión con corutinas (secuencial + cancelable)

Con corutinas, la forma “haz esto, luego aquello” es natural. Si el usuario cierra la pantalla, cancelar el scope detiene el trabajo en vuelo.

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

// Capa UI
val job = viewModelScope.launch { loginAndLoadProfile() }
override fun onCleared() { job.cancel() }

Versión con RxJava (cadena + disposal)

En RxJava, el mismo flujo es una cadena. Cancelar significa hacer dispose, típicamente con 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() }

Un suite de tests mínimo aquí debería cubrir tres resultados: éxito, fallos mapeados (401, 500s, sin red) y cancelación/dispose.

Próximos pasos: define convenciones y mantenlas

Los equipos suelen tener problemas porque los patrones varían entre features, no porque Kotlin Coroutines vs RxJava sea inherentemente “malo”. Una nota de decisión corta (incluso una página) ahorra tiempo en revisiones y hace el comportamiento predecible.

Empieza con una división clara: trabajo one-shot vs streams (actualizaciones a lo largo del tiempo). Decide la opción por defecto para cada uno y define cuándo se permiten excepciones.

Luego añade un pequeño conjunto de helpers compartidos para que cada feature se comporte igual cuando la red falle:

  • Un lugar para mapear errores (códigos HTTP, timeouts, offline) a fallos a nivel app que la UI entienda
  • Valores por defecto de timeout para llamadas de red, con forma clara de sobrescribir para operaciones largas
  • Una política de reintentos que indique qué es seguro reintentar (ej. GET vs POST)
  • Una regla de cancelación: qué se detiene cuando el usuario sale y qué puede continuar
  • Reglas de logging que ayuden al soporte sin filtrar datos sensibles

Las convenciones de testing importan igual. Acordad un enfoque estándar para que las pruebas no dependan de tiempo real o hilos reales. Para corutinas suele significar un test dispatcher y scopes estructurados. Para RxJava suele significar test schedulers y disposal explícito. Sea cual sea, apunta a tests rápidos y deterministas sin sleeps.

Si quieres avanzar más rápido en general, AppMaster (appmaster.io) es una opción para generar APIs de backend y apps móviles Kotlin sin escribir todo desde cero. Incluso con código generado, las mismas convenciones de producción alrededor de cancelación, errores, reintentos y testing mantienen el comportamiento de networking predecible.

FAQ

¿Cuándo debo usar una función suspend vs un stream para networking?

Predetermina suspend para una petición que devuelve una vez, como login o obtener el perfil. Usa Flow (o flujos de Rx) cuando los valores cambian con el tiempo, por ejemplo mensajes de websocket, conectividad o actualizaciones de base de datos.

¿La cancelación de corutinas realmente detiene una petición HTTP en curso?

Sí, pero solo si tu cliente HTTP soporta cancelación. Las corutinas detienen la coroutine cuando la scope se cancela, pero la llamada HTTP subyacente debe ser cancelable o la petición puede seguir ejecutándose en segundo plano.

¿Cuál es la forma más segura de evitar leaks cuando el usuario sale de una pantalla?

Asocia el trabajo a un scope del ciclo de vida, como viewModelScope, de modo que se cancele cuando termina la lógica de la pantalla. Evita lanzar en scopes de larga duración o globales a menos que el trabajo sea verdaderamente de toda la app.

¿Cómo debe diferir el manejo de errores entre Coroutines y RxJava?

En corutinas, las fallas suelen lanzarse y se manejan con try/catch cerca de donde puedes mapearlas al estado de UI. En RxJava, los errores viajan por el stream, así que mantén la ruta de error explícita y evita operadores que conviertan silenciosamente fallos en valores por defecto.

¿Debo modelar errores como excepciones o como datos?

Usa excepciones para fallos inesperados como timeouts, 500s o errores de parseo. Modela errores como datos tipados cuando la UI necesita una respuesta específica como “contraseña incorrecta” o “email ya usado”, así no dependes de comparar cadenas.

¿Cuál es una forma sencilla de añadir timeouts sin ensuciar el código?

Aplica el timeout donde puedas mostrar el mensaje correcto en la UI, es decir cerca del sitio de la llamada. En corutinas, withTimeout es directo para llamadas suspend; en RxJava, el operador timeout integra el timeout en la cadena.

¿Cómo implemento reintentos sin causar solicitudes duplicadas o baneos?

Reintenta solo cuando sea seguro, normalmente para peticiones idempotentes como GET, y limita el número (por ejemplo 2–3). No reintentes ante errores de validación o fallos de autenticación, y añade delays (con jitter) para no sobrecargar el servidor ni drenar batería.

¿Cuál es la trampa principal de hilos con Dispatchers vs Schedulers?

Las corutinas usan Dispatchers y normalmente empiezan en Main para la UI, luego cambian a IO o Default para trabajo pesado. RxJava usa subscribeOn para dónde corre el upstream y observeOn para dónde consumes resultados; coloca un observeOn(main) final antes de renderizar para evitar sorpresas.

¿Puedo mezclar RxJava y Coroutines durante una migración?

Sí, pero mantiene el límite pequeño y con soporte de cancelación. Envuelve Rx en suspend con un adaptador cancelable que haga dispose() si la corutina se cancela, y expón trabajo suspend a llamadores Rx solo mediante puentes documentados.

¿Cómo pruebo cancelación, reintentos y lógica basada en tiempo de forma fiable?

Usa tiempo virtual para que los tests no duerman ni dependan de hilos reales. En corutinas, runTest con un test dispatcher permite controlar delays y cancelaciones; en RxJava, usa TestScheduler y comprueba que no haya emisiones después de dispose().

Fácil de empezar
Crea algo sorprendente

Experimente con AppMaster con plan gratuito.
Cuando esté listo, puede elegir la suscripción adecuada.

Empieza