RxJava
RxJava is a reactive library for composing asynchronous and event-based streams. It is now a legacy choice for most new Android code, where Coroutines and Flow are usually preferred, but it remains common in production applications and migration work.
An Rx chain has three concerns:
- a source such as
Observable,Single, orFlowable; - operators that transform, combine, schedule, or observe emissions;
- a subscription represented by a
Disposable.
Unless an operator states otherwise, an error terminates the stream. Disposing a subscription stops downstream delivery, but cancellation of the underlying work depends on whether the source supports it.
Core reactive types
Observable and Flowable
Observable<T> emits zero or more items and does not implement the Reactive Streams backpressure protocol. It fits UI events and ordinary streams where the producer cannot overwhelm the consumer or where sampling and dropping are explicitly acceptable.
Flowable<T> is designed for streams whose producer may be faster than downstream. Backpressure means downstream can request a bounded number of items rather than accepting an unlimited push rate. It is useful for large or high-frequency streams, not automatically for every database or network call.
When adapting a non-backpressured source with toFlowable(strategy), choose a strategy from the data contract:
BUFFERkeeps pending items, but an unbounded buffer can exhaust memory;DROPdiscards items that cannot be delivered in time;LATESTkeeps only the newest pending item;ERRORreportsMissingBackpressureExceptionwhen demand is insufficient;MISSINGadds no strategy at the boundary, so a downstream backpressure operator must handle it.
Do not treat a strategy as a performance switch. Dropping a payment event is incorrect, while dropping intermediate sensor readings may be acceptable.
Single, Maybe, and Completable
Single<T>emits exactly one value or an error.Maybe<T>emits one value, completes empty, or reports an error.Completableemits no value and only completes or reports an error.
fun loadUser(id: String): Single<User>
fun findCachedUser(id: String): Maybe<User>
fun updateUser(user: User): Completable
Choose the narrowest type that expresses the contract. A possibly missing database row is usually a Maybe, while a save whose result is only success or failure is a Completable.
Transforming inner sources
map and flatMap
map performs a one-to-one synchronous transformation. It does not introduce another reactive source.
flatMap maps each item to an inner source and merges its emissions into the outer stream. It is suitable for dependent asynchronous work.
flatMap, concatMap, and switchMap
These operators differ in concurrency, ordering, and cancellation:
flatMapmay subscribe to several inner sources concurrently and does not preserve their emission order;concatMapwaits for each inner source to complete before subscribing to the next, preserving source order;concatMapEagersubscribes eagerly but buffers results to emit them in source order;switchMapdisposes the previous inner subscription when a new source item arrives and keeps only the latest inner source active.
switchMap is useful for search queries, but disposal is not a transaction rollback. A disposed network request may still reach the server, so writes must not rely on switchMap for correctness.
queryChanges
.debounce(300, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
.switchMap { query -> api.search(query).toObservable() }
Scheduling
subscribeOn and observeOn
subscribeOn(scheduler) schedules subscription to the upstream source. Its position in a simple chain does not change which upstream source it affects. With multiple subscribeOn calls, the one closest to the source normally controls where the source subscription begins; extra switches make behavior harder to reason about.
observeOn(scheduler) moves downstream notifications to that scheduler from that point until another observeOn changes it. It also introduces an asynchronous boundary and buffering, so unnecessary switches have a cost.
api.loadUser()
.subscribeOn(Schedulers.io())
.map { it.toUiModel() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ uiModel -> render(uiModel) },
{ error -> showError(error) },
)
Schedulers.io() is intended for blocking I/O and may grow its thread pool. CPU-intensive work belongs on Schedulers.computation(). UI access belongs on AndroidSchedulers.mainThread().
Creating sources
just, fromIterable, and defer
just(value) captures and emits the supplied value. Observable.just(list) emits one item - the list itself. fromIterable(list) emits every list element separately.
Observable.just(listOf("A", "B")) // Observable<List<String>>
Observable.fromIterable(listOf("A", "B")) // Observable<String>
defer creates a new source for each subscriber at subscription time. Use it when a value or decision must be evaluated lazily rather than captured while the chain is assembled.
Combining streams
mergeforwards emissions from all sources as they arrive; cross-source order is not guaranteed.concatsubscribes to sources sequentially and waits for each one to complete.zipcombines the first item from each source, then the second item from each, and so on.combineLatestemits after every source has emitted once, then reacts whenever any source changes.withLatestFromemits only when the primary source emits and adds the latest values from the other sources.switchOnNextconsumes a stream of streams and forwards only the latest inner stream.
Observable.combineLatest(user, settings) { userValue, settingsValue ->
UiModel(userValue, settingsValue)
}
Be careful with sources that never complete: a concat chain may never reach its next source, and zip may wait forever for the next matching item.
Side effects, errors, and disposal
doOnNext, doOnError, doOnComplete, doOnSubscribe, doOnDispose, and doFinally observe lifecycle events without changing the normal emissions. They are useful for logging, metrics, tracing, and cleanup. Keep business state transitions in explicit operators or subscribers instead of hiding them in doOn... callbacks.
repository.observeItems()
.doOnNext { items -> logger.log("items=${items.size}") }
.doOnError(logger::log)
.doFinally { progress.hide() }
Always define an error path. A one-argument subscribe has no application error handler, and unhandled errors may reach RxJavaPlugins.onError. Use onErrorReturn, onErrorResumeNext, or retry only when their semantics match the operation. Unbounded retry loops can amplify outages or repeat non-idempotent writes.
On Android, bind subscriptions to an explicit owner. A CompositeDisposable is commonly cleared when the corresponding screen, presenter, or component is destroyed. A retained subscription that captures an Activity, Fragment, or View can leak it or deliver a late UI update.
Migrating to Coroutines and Flow
Migrate at boundaries rather than rewriting every chain at once. First preserve behavior with tests, including ordering, cancellation, retry, and empty-result semantics. Then replace one API or feature and keep adapters at the boundary while both models coexist.
Typical conceptual mappings include:
Single<T>->suspend () -> T;Maybe<T>->suspend () -> T?when absence is the only empty outcome;Completable->suspend () -> Unit;Observable<T>/Flowable<T>->Flow<T>with an explicitly chosen buffering policy;switchMap->flatMapLatest,concatMap->flatMapConcat.
The types are not mechanically equivalent. Rx disposal and coroutine cancellation propagate differently, Flow is cold by default, and backpressure strategies do not map one-to-one to Flow buffering. Review ownership and cancellation at every bridge.
Related topics: Flow Basics, Flow Operators, and Coroutine Scopes & Cancellation.
References: RxJava project and documentation, ReactiveX operator reference.