Skip to main content

Command Palette

Search for a command to run...

RxJava in Android: The Complete Guide (From Basics to Production Patterns)

Updated
10 min read
RxJava in Android: The Complete Guide (From Basics to Production Patterns)

If you've ever written a callback inside a callback inside another callback just to do a network call, then show a result, then save to DB, you already understand why RxJava exists. It turns that messy, hard-to-read async code into clean, composable pipelines.

This guide covers everything: what RxJava is, how it works under the hood, all the operators you need to know, threading, error handling, memory management, and real-world patterns you'd actually use in production Android apps

What is RxJava? (And Why Should You Care?)

RxJava is a Reactive Extensions library for Java and Kotlin. It's built around one idea: treat data as a stream.

Whether it's a single API response, a list from a database, or a stream of button clicks, RxJava models all of it as an Observable stream that emits items over time. You attach an Observer to that stream, and it reacts to whatever gets emitted.

Think of it as a pipe-and-bucket model:

  • The pipe is your Observable (data flows through it)

  • The bucket is your Observer (it collects what comes out)

  • The valves and filters in between (are your operators)

The real power? You can transform, filter, combine, and control those streams with a rich set of operators and keep all your async code readable and maintainable.

The 5 Observable Types

Not every data source behaves the same way. RxJava gives you specialized types so you can be precise about what a stream will emit. This also helps the compiler and your teammates understand intent immediately.

1. Observable<T> — The General Purpose Stream

Emits 0 to many items, then completes or errors.

kotlin

Observable.fromArray("Delhi", "Mumbai", "Bangalore")
    .subscribe { city -> println(city) }

Use when: UI events, real-time updates, lists of unknown size.

2. Single<T> — Exactly One Result

Emits one item or an error. No onComplete — emission is completion.

kotlin

Single.just(currentUser)
    .subscribe(
        { user -> showProfile(user) },
        { error -> showError(error) }
    )

Use when: API calls that return a single response (login, fetch user profile).

3. Maybe<T> — Zero or One Result

Emits 0 or 1 item, then completes (or errors). The "it might or might not exist" type.

kotlin

Maybe.fromCallable { cache.getUser(userId) } // returns null if not found
    .subscribe(
        { user -> showFromCache(user) },
        { error -> handleError(error) },
        { loadFromNetwork() } // onComplete — nothing in cache
    )

Use when: Cache lookups, optional config values, nullable DB queries.

4. Completable — Fire and Forget

Emits no items — only a completion signal or error. You only care whether it succeeded.

kotlin

Completable.fromAction { database.saveUser(user) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        { showSuccessToast() },
        { error -> showError(error) }
    )

Use when: Saving to DB, logging analytics events, sending a fire-and-forget request.

5. Flowable<T> — High-Volume Streams with Backpressure

Like Observable, but designed for when the producer is faster than the consumer. Has built-in backpressure strategies.

kotlin

Flowable.range(1, 1_000_000)
    .onBackpressureBuffer()
    .observeOn(Schedulers.computation())
    .subscribe { process(it) }

Use when: Reading large files, sensor data, paginated DB queries, any stream that could overwhelm the subscriber.

Backpressure explained: Imagine a fire hose filling a small cup. The cup overflows. Backpressure is your strategy for that, buffer the excess, drop items, or only keep the latest. Observable has no such strategy and will crash with MissingBackpressureException. Flowable handles it gracefully.

Threading: subscribeOn vs observeOn

This is where most Android bugs come from. Get this wrong and you'll either crash with a CalledFromWrongThreadException or block the UI thread.

apiService.getUser(userId)
    .subscribeOn(Schedulers.io())             // network call runs on IO thread
    .observeOn(AndroidSchedulers.mainThread()) // result delivered on main thread
    .subscribe { user -> updateUI(user) }

The golden rule:

  • subscribeOn tells the SOURCE where to run

    It works upstream, meaning it decides on which thread the Observable starts emitting. It doesn't matter where in the chain you put it, only the first one wins if you add multiple.

    Observable.fromCallable { fetchFromDB() }  // ← runs on IO, because of ↓
        .subscribeOn(Schedulers.io())           // sets the SOURCE thread
    
  • observeOn switches thread for everything BELOW it

    It works downstream from the point you place it. Every operator and the final subscribe {} after this call runs on the new thread.

    .observeOn(AndroidSchedulers.mainThread())  // ← switch to UI thread
    .subscribe { textView.text = it }           // ← now safe 
    to touch UI
    

You can use observeOn multiple times in one chain to hop between threads:

kotlin

Observable.fromCallable { readFile() }
    .subscribeOn(Schedulers.io())           // file read → IO thread
    .map { parse(it) }                      // parsing → still IO
    .observeOn(Schedulers.computation())    // switch → computation
    .map { heavyProcessing(it) }            // heavy work → computation
    .observeOn(AndroidSchedulers.mainThread()) // switch → main
    .subscribe { updateUI(it) }             // UI update → main ✓

One-line memory trick:

subscribeOn = "where does the water come from?" observeOn = "where does the water go next?"

Scheduler Reference

Scheduler Use For
Schedulers.io() Network calls, DB access, file I/O
Schedulers.computation() CPU-bound work (parsing, encryption)
Schedulers.single() Sequential tasks, one thread
Schedulers.newThread() New thread per task (use sparingly, expensive)
AndroidSchedulers.mainThread() All UI updates

Operators: The Heart of RxJava

Operators sit between your Observable and Observer and transform the stream. This is where RxJava really shines.

Transformation Operators

map() Transform each item, one-to-one.

apiService.getUser()
    .map { user -> user.displayName }    // User → String
    .subscribe { name -> showName(name) }

flatMap() Transform each item into a new Observable, then merge all results. Order not guaranteed.

// Login, then use token to fetch accounts
authService.login(email, password)
    .flatMap { token -> accountService.getAccounts(token) }
    .subscribe { accounts -> showAccounts(accounts) }

concatMap() Like flatMap, but sequential. Waits for each inner Observable to complete before starting the next. Order guaranteed.

// Submit logs one by one, in order
Observable.fromIterable(pendingLogs)
    .concatMap { log -> apiService.submitLog(log) }
    .subscribe { showSubmitSuccess() }

switchMap() Cancels the previous inner Observable when a new item arrives. Only the latest matters.

// Classic search: cancel old search when user types again
RxTextView.textChanges(searchEditText)
    .debounce(300, TimeUnit.MILLISECONDS)
    .switchMap { query -> apiService.search(query) }
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { results -> showResults(results) }

flatMap vs concatMap vs switchMap is one of the most common RxJava interview questions. Remember: flatMap = merge (unordered), concatMap = sequential, switchMap = cancel previous.

Filtering Operators

.filter { amount -> amount > 1000 }         // only pass items that match
.take(5)                                     // only first 5 items, then complete
.skip(2)                                     // skip first 2 items
.distinct()                                  // skip duplicates (all-time)
.distinctUntilChanged()                      // skip consecutive duplicates
.debounce(300, TimeUnit.MILLISECONDS)        // wait for pause before emitting (search input)
.throttleFirst(1000, TimeUnit.MILLISECONDS)  // emit first, ignore rest for 1s (button click)

Combining Operators

zip() — Combine two streams pairwise. Emits only when both have emitted.

// Two parallel API calls, combine results
Single.zip(
    apiService.getUserProfile(),
    apiService.getUserPosts()
) { profile, posts ->
    DashboardData(profile, posts)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { dashboard -> renderDashboard(dashboard) }

merge() — Combine two streams, emit items as they arrive. No pairing.

Observable.merge(localDataStream, remoteDataStream)
    .subscribe { data -> updateList(data) }

combineLatest() — Emit when either source emits, using the latest value from both.

// Form validation: re-validate whenever either field changes
Observable.combineLatest(
    emailChanges,
    passwordChanges
) { email, password ->
    isValidEmail(email) && password.length >= 8
}
.subscribe { isValid -> submitButton.isEnabled = isValid }

Error Handling

RxJava errors are terminal once onError fires, the stream ends. Plan your error handling explicitly.

// Provide a fallback value on error
apiService.getUser()
    .onErrorReturn { error -> guestUser }

// Switch to a fallback Observable on error
apiService.getUser()
    .onErrorResumeNext { error -> cacheService.getUser() }

// Retry up to 3 times
apiService.getUser()
    .retry(3)

// Retry with exponential backoff
apiService.getUser()
    .retryWhen { errors ->
        errors.zipWith(Observable.range(1, 3)) { _, attempt -> attempt }
              .flatMap { attempt ->
                  Observable.timer(attempt * 2L, TimeUnit.SECONDS)
              }
    }

Subjects: Observable and Observer in One

A Subject is both an Observable (you can subscribe to it) and an Observer (you can push values into it). This makes it useful for bridging non-reactive code with reactive streams.

val subject = PublishSubject.create<String>()

subject.subscribe { event -> println(event) }

subject.onNext("user_clicked_buy") // push from anywhere subject.onNext("user_navigated_back")

Subject Behavior
PublishSubject Only emits to current subscribers. Past items lost.
BehaviorSubject Replays the last item to new subscribers.
ReplaySubject Replays all past items to any new subscriber.
AsyncSubject Only emits the last item, and only on onComplete.

Android use cases:

  • PublishSubject → event bus (button clicks, navigation events)

  • BehaviorSubject → state holder (similar to StateFlow)

Memory Leak Prevention with CompositeDisposable

Every subscribe() call returns a Disposable. If you forget to dispose it when the screen is destroyed, RxJava holds a reference to your Activity/Fragment — memory leak.

The idiomatic fix is CompositeDisposable in your ViewModel:

class UserViewModel : ViewModel() {

    private val disposables = CompositeDisposable()

    fun loadUser(userId: String) {
        val d = userRepository.getUser(userId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                { user -> _userState.value = user },
                { error -> _errorState.value = error.message }
            )
        disposables.add(d)
    }

    override fun onCleared() {
        disposables.clear() // cancels all subscriptions
        super.onCleared()
    }
}

Real-World Production Patterns

Pattern 1: Search with Debounce

RxTextView.textChanges(searchBox)
    .skipInitialValue()
    .debounce(300, TimeUnit.MILLISECONDS)
    .distinctUntilChanged()
    .filter { query -> query.length >= 2 }
    .switchMap { query ->
        apiService.search(query)
            .onErrorReturnItem(emptyList())
    }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { results -> adapter.submitList(results) }

Pattern 2: Parallel API Calls

Single.zip(
    apiService.getUser().subscribeOn(Schedulers.io()),
    apiService.getNotifications().subscribeOn(Schedulers.io()),
    apiService.getRecentOrders().subscribeOn(Schedulers.io())
) { user, notifications, orders ->
    HomeScreenData(user, notifications, orders)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { data -> renderHomeScreen(data) }

Pattern 3: Sequential Dependent Calls

// Login → get token → fetch profile with token
authService.login(credentials)
    .flatMap { token -> profileService.getProfile(token) }
    .flatMap { profile -> settingsService.getSettings(profile.id) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        { settings -> navigateToDashboard(settings) },
        { error -> showLoginError(error) }
    )

RxJava vs Kotlin Coroutines + Flow

Here's the honest answer:

RxJava Coroutines + Flow
Language Java + Kotlin Kotlin only
Operator richness Extensive (100+) Growing
Backpressure Flowable Flow (built-in)
Learning curve Steep Moderate
Jetpack Compose Limited Native
Industry direction Legacy codebases Greenfield standard

The honest take: For new Android projects today, Kotlin Coroutines + Flow is the preferred choice, it's more idiomatic Kotlin, has better tooling, and integrates seamlessly with Jetpack. But RxJava still powers a massive amount of production code. If you're joining a company with an existing codebase (especially fintech, e-commerce, or older product companies), there's a solid chance RxJava is running their core flows. Understanding it deeply makes you valuable in both worlds.

Summary: When to Use What

Scenario Use
Single API call Single
Optional/nullable result Maybe
Write operation, no result Completable
List / stream of items Observable
High-volume / fast stream Flowable
Search box debounce + switchMap
Parallel calls zip
Sequential dependent calls flatMap / concatMap
Form validation combineLatest
Event bus PublishSubject
State holder BehaviorSubject

RxJava has a steep learning curve, but once it clicks, you start seeing your entire codebase differently as data flowing through transformable pipelines. That mental model alone makes you a better developer, even if you switch to Coroutines on your next project.

Thanks for reading.