Kotlin Coroutines vs RxJava para rede e trabalho em background
Kotlin Coroutines vs RxJava: compare cancelamento, tratamento de erros e padrões de teste para rede e trabalho em background em apps Android reais.

Por que essa escolha importa para redes em produção
Networking e trabalho em background em um app Android real são mais que uma chamada de API. Incluem login e refresh de token, telas que podem ser rotacionadas durante o carregamento, sincronização depois que o usuário sai de uma tela, uploads de fotos e trabalho periódico que não pode drenar a bateria.
Os bugs mais danosos normalmente não são questões de sintaxe. Eles aparecem quando trabalho assíncrono vive além da UI (leaks), quando o cancelamento para a UI mas não a requisição real (tráfego desperdiçado e spinners travados), quando retries multiplicam requisições (limites de taxa, bans), ou quando camadas diferentes tratam erros de maneiras distintas de modo que ninguém consegue prever o que o usuário verá.
A decisão entre Kotlin Coroutines vs RxJava afeta a confiabilidade do dia a dia:
- Como você modela o trabalho (chamadas únicas vs streams)
- Como o cancelamento se propaga
- Como os erros são representados e mostrados para a UI
- Como você controla threads para rede, disco e UI
- Quão testáveis são temporizações, retries e casos de borda
Os padrões abaixo focam no que tende a quebrar sob carga ou em redes lentas: cancelamento, tratamento de erros, retries e timeouts, e hábitos de teste que previnem regressões. Exemplos ficam curtos e práticos.
Modelos mentais centrais: chamadas suspend, streams e Flow
A diferença principal entre Kotlin Coroutines vs RxJava é a forma do trabalho que você está modelando.
Uma função suspend representa uma operação única. Ela retorna um valor ou lança uma falha. Isso corresponde à maioria das chamadas de rede: buscar perfil, atualizar configurações, enviar uma foto. O código chamador é lido de cima para baixo, o que se mantém fácil de enxergar mesmo após adicionar logging, cache e ramificações.
O RxJava começa perguntando se você lida com um valor ou muitos valores ao longo do tempo. Um Single é um resultado de uma vez (sucesso ou erro). Um Observable (ou Flowable) é um stream que pode emitir muitos valores, então completar, ou falhar. Isso se encaixa em recursos que são verdadeiramente event-like: eventos de alteração de texto, mensagens de websocket ou polling.
Flow é a forma amigável a coroutines de representar um stream. Pense nele como a “versão stream” das coroutines, com cancelamento estruturado e encaixe direto com APIs suspensas.
Uma regra prática rápida:
- Use
suspendpara uma requisição e uma resposta. - Use
Flowpara valores que mudam ao longo do tempo. - Use RxJava quando seu app já depende fortemente de operadores e composição complexa de streams.
À medida que as features crescem, a legibilidade costuma quebrar primeiro quando você força um modelo de stream em uma chamada única, ou tenta tratar eventos contínuos como um único valor de retorno. Combine a abstração com a realidade primeiro, depois crie convenções ao redor.
Cancelamento na prática (com exemplos curtos)
Cancelamento é onde o código assíncrono ou parece seguro, ou se transforma em crashes aleatórios e chamadas desperdiçadas. O objetivo é simples: quando o usuário sai de uma tela, qualquer trabalho iniciado para aquela tela deve parar.
Com Kotlin Coroutines, cancelamento já faz parte do modelo. Um Job representa trabalho, e com concorrência estruturada você geralmente não passa jobs por aí. Você inicia trabalho dentro de um scope (como o scope do ViewModel). Quando esse scope é cancelado, tudo dentro também é cancelado.
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
}
}
Dois detalhes de produção importam:
- Chame rede em funções suspend através de um cliente cancelável. Caso contrário, a coroutine para mas a chamada HTTP pode continuar rodando.
- Use
withTimeout(ouwithTimeoutOrNull) para requisições que não podem travar.
RxJava usa disposal explícito. Você mantém um Disposable para cada subscription, ou os coleta em um CompositeDisposable. Quando a tela some, você dispose, e a cadeia deve parar.
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
}
}
Uma regra prática ao sair da tela: se você não consegue apontar onde o cancelamento acontece (cancelamento de scope ou dispose()), assuma que o trabalho continuará rodando e corrija antes de enviar para produção.
Tratamento de erros que permaneça compreensível
Uma grande diferença entre Kotlin Coroutines vs RxJava é como os erros viajam. Coroutines fazem as falhas parecerem código normal: uma chamada suspend lança, e o chamador decide o que fazer. Rx empurra falhas através do stream, o que é poderoso, mas fácil de esconder problemas se você não tomar cuidado.
Use exceções para falhas inesperadas (timeouts, 500s, bugs de parsing). Modele erros como dados quando a UI precisa de uma resposta específica (senha errada, “email já usado”) e você quer que isso faça parte do seu modelo de domínio.
Um padrão simples com coroutines preserva a stack trace e continua legível:
suspend fun loadProfile(): Profile = try {
api.getProfile() // may throw
} catch (e: IOException) {
throw NetworkException("No connection", e)
}
runCatching e Result são úteis quando você realmente quer retornar sucesso ou falha sem lançar:
suspend fun loadProfileResult(): Result<Profile> =
runCatching { api.getProfile() }
Cuidado com getOrNull() se você não estiver também tratando a falha. Isso pode transformar silenciosamente bugs reais em telas de “estado vazio”.
No RxJava, mantenha o caminho de erro explícito. Use onErrorReturn apenas para fallbacks seguros. Prefira onErrorResumeNext quando precisar trocar de fonte (por exemplo, para dados em cache). Para retries, mantenha regras estreitas com retryWhen para não retryar em casos como “senha errada”.
Um conjunto de hábitos que evita erros engolidos:
- Logue ou reporte um erro uma vez, próximo do ponto onde você tem contexto.
- Preserve a exceção original como
causeao embrulhar. - Evite fallbacks catch-all que transformam todo erro em um valor padrão.
- Faça erros voltados ao usuário como um modelo tipado, não uma string.
Noções básicas de threading: Dispatchers vs Schedulers
Muitos bugs assíncronos se reduzem a threading: executar trabalho pesado na main, ou tocar a UI de uma thread de background. Kotlin Coroutines vs RxJava difere principalmente em como você expressa trocas de thread.
Com coroutines, você normalmente começa na thread principal para trabalho de UI, então salta para um dispatcher de background para as partes caras. Escolhas comuns são:
Dispatchers.Mainpara atualizações de UIDispatchers.IOpara I/O bloqueante como rede e discoDispatchers.Defaultpara trabalho de CPU como parsing JSON, ordenar, criptografia
Um padrão direto é: buscar dados, parsear fora da main, então 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 expressa “onde o trabalho acontece” com subscribeOn e “onde os resultados são observados” com observeOn. Uma surpresa comum é esperar que observeOn afete o upstream. Não afeta. subscribeOn define a thread para a fonte e operadores acima dela, e cada observeOn muda threads a partir daquele ponto.
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) }
)
Uma regra que evita surpresas: mantenha trabalho de UI em um só lugar. Em coroutines, atribua ou colete estado de UI em Dispatchers.Main. Em RxJava, coloque um único observeOn(main) logo antes de renderizar e não espalhe observeOn extras a menos que realmente precise.
Se uma tela engasga, mova parsing e mapeamento para fora da main primeiro. Essa mudança simples resolve muitos problemas do mundo real.
Retries, timeouts e trabalho paralelo para chamadas de rede
O caminho feliz raramente é o problema. As questões vêm de chamadas que travam, retries que pioram a situação, ou trabalho “paralelo” que na prática não é paralelo. Esses padrões frequentemente decidem por que um time prefere Kotlin Coroutines vs RxJava.
Timeouts que falham rápido
Com coroutines, você pode colocar um limite rígido ao redor de qualquer chamada suspend. Mantenha o timeout perto do local da chamada para poder mostrar a mensagem certa na UI.
val user = withTimeout(5_000) {
api.getUser() // suspend
}
Em RxJava, você anexa um operador de timeout ao stream. Isso é útil quando o comportamento de timeout deve fazer parte de um pipeline compartilhado.
Retries sem causar danos
Retry apenas quando for seguro. Uma regra simples: retry em requisições idempotentes (como GET) mais livremente do que em requisições que criam efeitos colaterais (como “criar pedido”). Mesmo assim, limite a contagem e adicione delay ou jitter.
Bons guardrails por padrão:
- Retry em timeouts de rede e erros temporários do servidor.
- Não retry em erros de validação (400s) ou falhas de auth.
- Limite retries (frequentemente 2–3) e logue a falha final.
- Use backoff delays para não bombardear o servidor.
No RxJava, retryWhen permite expressar “retry apenas para esses erros, com esse delay”. Em coroutines, Flow tem retry e retryWhen, enquanto funções suspend simples costumam usar um pequeno loop com delays.
Chamadas paralelas sem código emaranhado
Coroutines tornam trabalho paralelo direto: inicie duas requisições e espere ambas.
coroutineScope {
val profile = async { api.getProfile() }
val feed = async { api.getFeed() }
profile.await() to feed.await()
}
RxJava brilha quando combinar múltiplas fontes é o ponto principal da cadeia. zip é a ferramenta usual para “esperar ambos”, e merge é útil quando você quer resultados assim que chegarem.
Para streams grandes ou rápidos, backpressure ainda importa. Flowable do RxJava tem ferramentas maduras de backpressure. Flow das coroutines lida bem com muitos casos, mas você ainda pode precisar de políticas de buffering ou dropping se eventos puderem ultrapassar sua UI ou gravações no banco.
Interop e padrões de migração (codebases mistas)
A maioria das equipes não troca tudo de uma vez. Uma migração prática entre Kotlin Coroutines vs RxJava mantém o app estável enquanto você move módulo a módulo.
Envolver uma API Rx em uma função suspend
Se você tem um Single<T> ou Completable existente, envolva-o com suporte a cancelamento para que uma coroutine cancelada desfaça a subscription 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() }
}
Isso evita um modo de falha comum: o usuário sai da tela, a coroutine é cancelada, mas a chamada de rede continua rodando e atualiza estado compartilhado depois.
Expor código coroutine para chamadores Rx
Durante a migração, algumas camadas ainda vão esperar tipos Rx. Envolva trabalho suspend em Single.fromCallable e bloqueie apenas em uma thread de background.
fun loadProfileRx(api: Api): Single<Profile> =
Single.fromCallable {
runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
}
Mantenha essa fronteira pequena e documentada. Para código novo, prefira chamar a API suspend diretamente de um scope de coroutine.
Onde Flow se encaixa, e onde não
Flow pode substituir muitos casos de Observable: estado de UI, atualizações de banco de dados e streams tipo paging. Pode ser menos direto se você depende fortemente de streams “quentes”, subjects, tuning avançado de backpressure, ou um grande conjunto de operadores customizados que seu time já domina.
Uma estratégia de migração que reduz confusão:
- Converta módulos folha primeiro (rede, armazenamento) para APIs suspend.
- Adicione pequenos adaptadores nas fronteiras de módulo (Rx para suspend, suspend para Rx).
- Substitua streams Rx por Flow apenas quando também controlar os consumidores.
- Mantenha um estilo assíncrono por área de feature.
- Delete adaptadores assim que o último chamador migrar.
Padrões de teste que você realmente usará
Problemas de temporização e cancelamento são onde bugs assíncronos se escondem. Bons testes assíncronos tornam o tempo determinístico e os resultados fáceis de afirmar. Essa é outra área onde Kotlin Coroutines vs RxJava parece diferente, mesmo que ambos possam ser bem testados.
Coroutines: runTest, TestDispatcher e controlar o tempo
Para código com coroutines, prefira runTest com um test dispatcher para que seu teste não dependa de threads reais ou delays reais. Tempo virtual deixa você disparar timeouts, retries e janelas de debounce sem dormir o teste.
@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()
}
Para testar cancelamento, cancele o Job que coleta (ou o scope pai) e afirme que sua API fake para ou que não há mais estados emitidos.
RxJava: TestScheduler, TestObserver, tempo determinístico
Testes Rx geralmente combinam um TestScheduler para tempo e um TestObserver para asserções.
@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
}
Ao testar caminhos de erro em qualquer estilo, foque no mapeamento, não no tipo exato da exceção. Afirme o estado de UI que você espera após um 401, um timeout ou uma resposta malformada.
Um pequeno conjunto de checagens cobre a maioria das regressões:
- Estados de Loading e final (Success, Empty, Error)
- Cleanup de cancelamento (job cancelado, disposable disposed)
- Mapeamento de erro (códigos HTTP para mensagens de usuário)
- Sem emissões duplicadas após retries
- Lógica baseada em tempo usando tempo virtual, não delays reais
Erros comuns que causam bugs em produção
A maioria dos problemas de produção não vem de escolher Kotlin Coroutines vs RxJava. Vem de alguns hábitos que fazem o trabalho rodar mais tempo do que você pensa, rodar duas vezes ou tocar a UI na thread errada.
Um vazamento comum é lançar trabalho no scope errado. Se você inicia uma chamada de rede em um scope que vive mais que a tela (ou cria seu próprio scope e nunca cancela), a requisição pode terminar depois que o usuário saiu e ainda tentar atualizar estado. Em coroutines isso costuma parecer usar um scope de longa duração por padrão. Em RxJava, geralmente é um dispose esquecido.
Outro clássico é “fire and forget”. Scopes globais e Disposables esquecidos parecem bem até o trabalho se acumular. Uma tela de chat que faz refresh em cada resume pode facilmente acabar com múltiplos jobs de refresh após algumas navegações, cada um segurando memória e competindo pela rede.
Retries também são fáceis de errar. Retries ilimitados, ou sem delay, podem spammar seu backend e drenar bateria. É especialmente perigoso quando a falha é permanente, como um 401 após logout. Faça retries condicionais, adicione backoff e pare quando o erro não for recuperável.
Erros de threading causam crashes difíceis de reproduzir. Você pode parsear JSON na main ou atualizar UI de uma thread de background dependendo de onde colocou um dispatcher ou scheduler.
Checagens rápidas que pegam a maioria desses problemas:
- Vincule o trabalho a um owner de lifecycle e cancele quando esse owner terminar.
- Torne o cleanup óbvio: cancele Jobs ou limpe Disposables em um só lugar.
- Coloque limites estritos em retries (contagem, delay e quais erros qualificam).
- Implemente uma regra para updates de UI (apenas main) nas revisões de código.
- Trate sync background como um sistema com constraints, não uma função aleatória.
Se você publica apps Android a partir de código Kotlin gerado (por exemplo, do AppMaster), os mesmos riscos continuam. Você ainda precisa de convenções claras para scopes, cancelamento, limites de retry e regras de threading.
Checklist rápido para escolher Coroutines, RxJava ou ambos
Comece com a forma do trabalho. A maioria das chamadas de rede é single-shot, mas apps também têm sinais contínuos como conectividade, estado de auth ou updates ao vivo. Escolher a abstração errada cedo costuma aparecer depois como cancelamento bagunçado e caminhos de erro difíceis de ler.
Uma forma simples de decidir (e explicar a decisão ao time):
- Requisição única (login, buscar perfil): prefira uma função
suspend. - Stream contínuo (eventos, atualizações do banco): prefira
Flowou RxObservable. - Cancelamento vinculado ao lifecycle: coroutines em
viewModelScopeoulifecycleScopesão frequentemente mais simples que disposables manuais. - Forte dependência em operadores de stream avançados e backpressure: RxJava pode ainda ser a melhor opção, especialmente em codebases antigas.
- Retries complexos e mapeamento de erro: escolha a abordagem que seu time consegue manter legível.
Uma regra prática: se uma tela faz uma requisição e renderiza um resultado, coroutines deixam o código próximo de uma chamada de função normal. Se você constrói um pipeline de muitos eventos (digitação, debounce, cancelar requisições anteriores, combinar filtros), RxJava ou Flow costuma parecer mais natural.
Consistência vence perfeição. Dois padrões bons usados em todo lugar são mais fáceis de manter que cinco “melhores” padrões usados de maneira inconsistente.
Cenário de exemplo: login, busca de perfil e sync em background
Um fluxo comum em produção é: o usuário toca Login, você chama um endpoint de auth, então busca o perfil para a tela principal e finalmente inicia um sync em background. É aqui que Kotlin Coroutines vs RxJava pode parecer diferente no dia a dia de manutenção.
Versão com Coroutines (sequencial + cancelável)
Com coroutines, a forma “faça isso, depois aquilo” é natural. Se o usuário fechar a tela, cancelar o scope para o trabalho em andamento.
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() }
Versão RxJava (cadeia + disposal)
No RxJava, o mesmo fluxo é uma cadeia. Cancelamento significa dar dispose, tipicamente com um 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() }
Uma suíte de testes mínima aqui deve cobrir três resultados: sucesso, falhas mapeadas (401, 500s, sem rede) e cancelamento/dispose.
Próximos passos: defina convenções e mantenha-as
Times normalmente se metem em problemas porque padrões variam entre features, não porque Kotlin Coroutines vs RxJava é inerentemente “errado”. Uma nota de decisão curta (mesmo de uma página) economiza tempo em revisões e torna o comportamento previsível.
Comece com uma separação clara: trabalho one-shot (uma chamada de rede que retorna uma vez) vs streams (atualizações ao longo do tempo, como websocket, localização ou mudanças de banco). Decida o padrão default para cada um e defina quando exceções são permitidas.
Depois adicione um pequeno conjunto de helpers compartilhados para que todo recurso se comporte do mesmo jeito quando a rede falha:
- Um lugar para mapear erros (códigos HTTP, timeouts, offline) para falhas em nível de app que sua UI entende
- Valores padrão de timeout para chamadas de rede, com forma clara de sobrescrever para operações longas
- Uma política de retry que diz o que é seguro retryar (por exemplo, GET vs POST)
- Uma regra de cancelamento: o que para quando o usuário sai da tela e o que pode continuar
- Regras de logging que ajudam suporte sem vazar dados sensíveis
Convenções de teste importam tanto quanto. Concorde em uma abordagem padrão para que testes não dependam de tempo real ou threads reais. Para coroutines, isso geralmente significa um test dispatcher e scopes estruturados. Para RxJava, isso normalmente significa test schedulers e disposal explícito. Em ambos os casos, busque testes rápidos e determinísticos sem sleeps.
Se você quer acelerar a entrega, AppMaster (appmaster.io) é uma opção para gerar APIs backend e apps móveis em Kotlin sem escrever tudo do zero. Mesmo com código gerado, as mesmas convenções de produção em torno de cancelamento, erros, retries e testes são o que mantêm o comportamento de rede previsível.
FAQ
Padronize em suspend para uma requisição que retorna apenas uma vez, como login ou buscar perfil. Use Flow (ou streams do Rx) quando os valores mudam ao longo do tempo, como mensagens de websocket, conectividade ou atualizações do banco de dados.
Sim — mas somente se seu cliente HTTP suportar cancelamento. Coroutines cancelam a coroutine quando o escopo é cancelado, mas a chamada HTTP subjacente também precisa ser cancelável; caso contrário, a requisição pode continuar em segundo plano.
Vincule o trabalho a um escopo de lifecycle, como viewModelScope, para que ele seja cancelado quando a lógica da tela terminar. Evite lançar em scopes de longa duração ou globais, a menos que o trabalho seja realmente de escopo do app.
Em coroutines, falhas normalmente causam throw e você as trata com try/catch próximo do local onde faz o mapeamento para o estado da UI. Em RxJava, os erros percorrem a stream, então mantenha o caminho de erro explícito e evite operadores que transformam falhas em valores default silenciosamente.
Use exceções para falhas inesperadas como timeouts, 500s ou problemas de parsing. Modele erros como dados tipados quando a UI precisa de uma resposta específica — por exemplo “senha incorreta” ou “email já usado” — para não depender de comparação de strings.
Coloque o timeout perto do local da chamada, onde você pode exibir a mensagem correta na UI. Em coroutines, withTimeout é direto para chamadas suspend; em RxJava, o operador timeout inclui o timeout na cadeia.
Faça retry apenas quando for seguro — normalmente para requisições idempotentes como GET — e limite a quantidade (2–3 é comum). Não faça retry em erros de validação nem em falhas de autenticação e adicione delays/jitter para não sobrecarregar o servidor ou drenar a bateria.
O padrão é: comece na Main para trabalho de UI e troque para IO/Default para trabalho pesado. Em RxJava, subscribeOn define onde a fonte roda e operadores a montante; observeOn muda o contexto dali em diante. Mantenha uma única troca final para a thread principal antes de renderizar.
Sim, é possível, mas mantenha a fronteira pequena e com suporte a cancelamento. Envolva Rx em um suspend com um adaptador cancelável que dispose a assinatura ao cancelar a coroutine. Exponha trabalho suspend para chamadores Rx apenas em pontes documentadas e limitadas.
Use tempo virtual para que os testes não dependam de sleeps ou threads reais. Para coroutines, runTest com um test dispatcher permite controlar delays e cancelamentos; para RxJava, use TestScheduler e verifique que não há emissões após dispose().


