androidinterview.com

RxJava Interview Questions

15 questions

Tier
Difficulty
Level

Showing all 15 questions

Image Loading

How do the Android image loading libraries Glide and Fresco work internally?

Tier: CommonDifficulty: Hard

Glide and Fresco solve the same three problems internally, keep memory use down, avoid redundant work, and stay fast on repeat loads, through downsampling, a two level cache, and bitmap reuse.

  • Downsampling. Neither library decodes an image at its full resolution if the ImageView it's going into is smaller. A 2000 by 2000 source image loading into a 400 by 400 view gets decoded straight to roughly 400 by 400, using BitmapFactory.Options.inSampleSize, so the full resolution bytes never sit in memory in the first place.
  • Two level caching. A request checks an in memory cache of already decoded bitmaps first, then a disk cache of downloaded but possibly not yet decoded images, and only falls back to the network if both miss. A cache hit at either level skips the more expensive step below it, decode or download.
  • Bitmap pooling. Instead of letting a bitmap that's scrolled off screen get garbage collected and allocating a fresh one for the next image, both libraries keep a pool of already allocated bitmaps of compatible size and hand them back out, loading new pixel data into that existing memory through BitmapFactory.Options.inBitmap. This is what keeps scrolling a RecyclerView full of images from constantly triggering garbage collection pauses.
  • Lifecycle aware cancellation. Both libraries tie a request to the Activity or Fragment that started it, and cancel in flight decodes and downloads the moment that screen is destroyed, so scrolling past ten images doesn't leave ten downloads still running for views nobody can see anymore.

The net effect of all four together is that a RecyclerView full of images stays smooth precisely because most of the expensive work, downloading, decoding, allocating, only happens once per image, and every scroll after that is serving from memory or reusing an existing bitmap's backing array instead of doing that work again.

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

RxJava

Tell me something about RxJava.

Tier: Less commonDifficulty: Easy

RxJava is a library for composing asynchronous and event based work as streams, using an Observable that emits items over time and operators that transform, combine, or schedule those emissions.

Observable.fromCallable(() -> api.fetchUser(id))
    .subscribeOn(Schedulers.io())              // do the work on a background thread
    .observeOn(AndroidSchedulers.mainThread())  // deliver the result on the main thread
    .subscribe(
        user -> textView.setText(user.name),    // onNext
        error -> showError(error)               // onError
    );

The core idea is the observer pattern with a huge operator library layered on top, map, flatMap, filter, zip, debounce, and dozens more, so you describe a pipeline of transformations once and it runs the same way every time data flows through it. It's a reactive streams implementation, which means it also standardizes backpressure, how a fast producer and a slow consumer negotiate, through the Flowable type.

It's worth being direct about where RxJava sits in 2026, it's legacy on a new Android codebase. Kotlin Flow covers the same ground natively, map, flatMapLatest, debounce, and structured concurrency through coroutine scopes replace what Observable, flatMap, and CompositeDisposable did here, without a second reactive library and its learning curve on top of coroutines you already need. You'll still find RxJava in plenty of existing production apps and in interview questions testing whether you understand reactive streams generally, so it's worth knowing the vocabulary, but it's not what you'd reach for starting a project today.

What is the difference between Schedulers.io() and Schedulers.computation() in RxJava?

Tier: Less commonDifficulty: Easy

Schedulers.io() is a large, elastic thread pool meant for work that blocks waiting on something external, Schedulers.computation() is a small, fixed pool sized to the number of CPU cores, meant for work that keeps the CPU busy.

apiCall()
    .subscribeOn(Schedulers.io())          // network call, thread mostly waits, not computes
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(result -> render(result));

heavyComputation()
    .subscribeOn(Schedulers.computation())  // CPU bound, keep it off the small pool's few threads
    .subscribe(result -> render(result));

Schedulers.io() can grow well beyond the number of CPU cores, because a thread blocked on a network response or a disk read isn't using the CPU, it's just waiting, so having far more threads than cores is fine and often necessary to keep many I/O calls in flight at once. Schedulers.computation() deliberately caps its thread count near the core count, because CPU bound work actually competes for CPU time, and having more threads than cores just adds context switching overhead without more real throughput.

Running CPU heavy work, like parsing a large JSON payload or resizing a bitmap, on Schedulers.io() mostly works but wastes the pool's capacity for I/O, and running I/O on Schedulers.computation() risks starving that small pool since its threads are meant to stay busy computing, not sit blocked waiting on a socket. This maps directly onto Dispatchers.IO and Dispatchers.Default in coroutines, same split, same reasoning, blocking work on IO, CPU bound work on Default.

Explain Subjects in RxJava.

Tier: Less commonDifficulty: Medium

A Subject is both an Observer and an Observable at once, so it can subscribe to a source and re-emit whatever it receives to its own subscribers, which makes it RxJava's way of getting a hot, multicast stream you can push values into manually.

PublishSubject<String> publish = PublishSubject.create();    // only future emissions
ReplaySubject<String> replay = ReplaySubject.create();        // every emission, ever
BehaviorSubject<String> behavior = BehaviorSubject.createDefault("idle"); // latest, then future
AsyncSubject<String> async = AsyncSubject.create();            // only the final value, on completion

publish.onNext("event"); // pushed to any subscriber that was already listening
  • PublishSubject only emits items that occur after a given subscriber subscribes, anything emitted before that is missed.
  • ReplaySubject replays every item ever emitted to any new subscriber, regardless of when they join.
  • BehaviorSubject emits the most recently emitted item, or a seeded default, immediately on subscription, then continues with whatever comes after.
  • AsyncSubject only emits the final item, and only once the source has completed, everything before that is discarded.

The one to reach for depends on what a late subscriber should see, nothing before now, everything, just the latest, or only the end result. In Flow, MutableSharedFlow covers PublishSubject and ReplaySubject depending on its replay parameter, and MutableStateFlow covers BehaviorSubject, since it always holds a current value and emits it immediately to new collectors. There's no direct Flow equivalent to AsyncSubject, since a suspend fun already expresses "wait for the one final result" without needing a stream type at all.

How are the Timer, Delay and Interval operators used in RxJava?

Tier: Less commonDifficulty: Medium

All three are time based operators, timer emits a single value once after a delay, delay shifts every emission from an existing source later by a fixed amount, and interval emits an increasing integer repeatedly on a fixed period.

Observable.timer(2, TimeUnit.SECONDS)                 // one emission, once, after 2 seconds
    .subscribe(tick -> showSplashDone());

sourceObservable
    .delay(500, TimeUnit.MILLISECONDS)                  // same items, all shifted 500ms later
    .subscribe(item -> render(item));

Observable.interval(0, 1, TimeUnit.SECONDS)             // 0, 1, 2, 3, ... one per second, forever
    .take(60)                                            // stop after a minute
    .subscribe(second -> updateClock(second));

timer is the closest thing RxJava has to a one shot alarm, useful for something like dismissing a splash screen after a fixed wait. delay doesn't change what a source emits, only when, every item keeps its original order and value, just pushed back by the given duration, useful for something like debouncing a UI state that flickers. interval runs indefinitely on its own schedule, Schedulers.computation() by default, until you stop it, either by disposing the subscription or chaining .take(n) to cap how many ticks you want.

Kotlin Flow covers the same three needs with delay() inside a flow { } builder for a one shot wait, the .map or .onEach on an existing flow combined with delay for a shifted flow, and a manual while (true) { emit(n); delay(period) } loop, or the kotlinx.coroutines.flow.flow equivalent, in place of interval. There's no single operator standing in for interval, since a plain suspend loop with delay inside a flow builder does the same job with less API surface to remember.

How do you implement a search feature using RxJava in your application?

Tier: Less commonDifficulty: Medium

An instant search box turns text change events into an Observable and chains a specific set of operators onto it, debounce to wait for the user to pause typing, distinctUntilChanged to skip repeat queries, and switchMap to cancel any in flight request the moment a newer one comes in.

searchViewTextChanges()                            // PublishSubject fed from a TextWatcher
    .debounce(300, TimeUnit.MILLISECONDS)            // wait for a pause in typing
    .filter(query -> !query.isEmpty())
    .distinctUntilChanged()                          // skip if the query didn't actually change
    .switchMap(query -> searchApi(query)             // cancel the previous request, start the new one
        .subscribeOn(Schedulers.io()))
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(results -> showResults(results));

debounce(300, TimeUnit.MILLISECONDS) is what stops a network call from firing on every single keystroke, it only lets a value through once 300 milliseconds have passed without another one arriving. distinctUntilChanged() catches the case where the same query comes through twice in a row, like a character typed and immediately deleted. switchMap is the operator doing the real work here, unlike flatMap, it unsubscribes from the previous inner Observable the moment a new one starts, so a slow response to an outdated query never overwrites a newer, faster one landing after it.

The Flow version is the same three operators by name, debounce, distinctUntilChanged, and flatMapLatest in place of switchMap, chained onto a MutableStateFlow fed by the search box instead of a PublishSubject. The logic doesn't change, RxJava didn't have a worse idea here, it's genuinely the same pipeline, just expressed with coroutine primitives instead of Rx ones.

How do you implement pagination in RecyclerView using RxJava operators?

Tier: Less commonDifficulty: Medium

Pagination with RxJava turns scroll position into a stream of page numbers, and feeds each one into a request pipeline that fetches the page, appends it to the adapter, and keeps requests in order even if the user scrolls fast.

private final PublishProcessor<Integer> pageRequests = PublishProcessor.create();

pageRequests
    .onBackpressureDrop()                          // ignore extra scroll triggered requests
    .concatMapSingle(page -> api.getItems(page)     // one page request at a time, in order
        .subscribeOn(Schedulers.io())
        .onErrorReturn(error -> Collections.emptyList()))
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(items -> adapter.addItems(items));

// in the RecyclerView's scroll listener, once near the bottom
pageRequests.onNext(nextPage);

A scroll listener watching for the last visible item nearing the bottom of the list is what triggers pageRequests.onNext(nextPage). onBackpressureDrop() protects against a fast scroll firing several triggers before the first page even comes back. concatMapSingle is doing the real work, it's concatMap specialized for a Single returning function, and it keeps page requests strictly sequential, page 3 never starts fetching before page 2 has finished, which avoids pages landing in the adapter out of order. onErrorReturn keeps one failed page from killing the whole stream, so scrolling further can still trigger the next page request.

In Flow, the same shape uses a MutableSharedFlow<Int> for page requests, mapLatest or a manual sequential collect for the ordering concatMapSingle gave you, though in practice Paging 3, built on Flow, already solves this exact problem, request deduplication, sequential loading, error state, without you assembling the operator chain yourself, which is the bigger reason this whole hand rolled pipeline is legacy territory now.

How do you make two network calls in parallel using RxJava?

Tier: Less commonDifficulty: Medium

The zip operator runs multiple Observables concurrently and waits for all of them to emit before combining their results with a single function, which is exactly what running two independent network calls in parallel and then merging the results calls for.

Observable.zip(
    getUserObservable().subscribeOn(Schedulers.io()),
    getUserPostsObservable().subscribeOn(Schedulers.io()),
    (user, posts) -> new UserProfile(user, posts)   // runs once both have emitted
)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(profile -> render(profile));

Each source Observable needs its own subscribeOn(Schedulers.io()), since without it both calls would still run sequentially on whatever single thread called zip. With that in place, both requests fire at roughly the same time, and zip holds the combining function's result back until both have emitted, then calls it exactly once with both values.

This is a case where the Flow answer genuinely looks different, not just renamed. Kotlin's coroutineScope { } with two async { } calls, then reading both with awaitAll() or destructuring the two Deferred results, does the same parallel fetch and combine, and reads as ordinary sequential looking code instead of an operator pipeline, since async starts the work immediately and await just suspends until it's done. That directness, no operator to remember, no scheduler to wire up, is the main reason structured concurrency replaced RxJava for this specific pattern.

How will you handle errors in RxJava?

Tier: Less commonDifficulty: Medium

RxJava treats an error as a terminal event, the moment onError fires the stream is done, no more onNext calls follow, so error handling means either recovering inline with an operator or catching it in the onError callback of subscribe.

apiCall()
    .onErrorReturn(error -> Cache.getLast())        // swap in a fallback value, stream continues
    .onErrorResumeNext(error -> retryFromCache())    // swap in a whole fallback Observable
    .retry(2)                                        // resubscribe up to 2 times on error
    .subscribe(
        result -> render(result),
        error -> showErrorState(error)               // last resort, if nothing upstream recovered
    );
  • onErrorReturn supplies a single fallback value and lets the stream complete normally instead of erroring out.
  • onErrorResumeNext swaps in an entirely different Observable to continue from, useful when recovery itself needs to do more work, like reading from a cache.
  • retry(n) resubscribes to the source up to n times before giving up, for errors that are likely transient, like a flaky network call.
  • The onError callback passed to subscribe is the final backstop, if nothing upstream recovered, this is where you actually show the user something went wrong.

Forgetting the second argument to subscribe() entirely is the classic bug here, RxJava's default error handler just rethrows onto the wrong thread, which crashes the app with a stack trace that looks nothing like your actual code. In Flow, the same shape exists as catch { } for recovering inline and a plain try, catch around a collect block or the call site for the last resort, with the compiler at least warning you if you never handle a caught exception, which softens that classic RxJava foot gun.

What are the types of Observables in RxJava?

Tier: Less commonDifficulty: Medium

RxJava has five reactive types, and which one you reach for depends entirely on how many items the source can emit.

Observable<Integer> progress = Observable.create(emitter -> { /* 0, 25, 50, 100 */ });
Flowable<Byte> chunks = Flowable.create(emitter -> { /* thousands of items fast */ }, BackpressureStrategy.BUFFER);
Single<User> user = Single.fromCallable(() -> api.getUser(id));       // exactly one item, or an error
Maybe<Cache> cached = Maybe.fromCallable(() -> cache.read());          // zero or one item, or an error
Completable save = Completable.fromAction(() -> db.save(entity));      // no item, just done or error
  • Observable emits zero, one, or many items over time, with no backpressure handling built in. Use it for UI events or anything emitting at a moderate, human scale rate.
  • Flowable is the same idea but built for sources that can emit faster than the consumer can keep up, like reading a large file. It requires a BackpressureStrategy to say what happens when the buffer fills, drop items, buffer them, or apply backpressure upstream.
  • Single emits exactly one item or an error, nothing else, the natural fit for a network response that returns exactly one thing.
  • Maybe emits zero or one item, or an error, for the case where a result might legitimately not exist, like a cache lookup that can miss.
  • Completable never emits a value at all, it only signals completion or error, for a fire and forget action like a database write.

The Flow equivalent collapses most of this into one type. A Flow<T> handles zero, one, or many items and applies backpressure by suspending the producer automatically, so there's no separate Flowable. suspend fun alone covers what Single and Completable did, a suspend function either returns a value or throws, no wrapper type needed, which is one of the concrete ways coroutines simplified what RxJava needed five types to express.

What is the difference between Concat and Merge in RxJava?

Tier: Less commonDifficulty: Medium

concat runs its sources one after another, waiting for each to complete before starting the next, merge runs them all at once and interleaves whatever comes out.

Observable.concat(observableA, observableB) // A1, A2, A3, then B1, B2, B3, always in order
    .subscribe(System.out::println);

Observable.merge(observableA, observableB)  // A1, A2, B1, A3, B2, ... interleaved, no guaranteed order
    .subscribe(System.out::println);

concat preserves order at the cost of latency, observableB never starts emitting until observableA has fully completed, even if observableB was ready to go immediately. That makes it the right choice when the sequence matters, like playing a queue of sounds in order. merge subscribes to every source at once, so items come through as soon as each source produces them, with no guarantee about which source's item shows up next, which makes it the right choice when you just want everything as fast as possible and order across sources doesn't matter.

Flow keeps the same split under different names, flowOf(flowA, flowB).flattenConcat() behaves like concat, and merge(flowA, flowB) behaves like merge, using the same reasoning, sequential when order matters, concurrent when throughput matters and order doesn't.

What is the difference between the flatMap and map operators in RxJava?

Tier: Less commonDifficulty: Medium

map transforms each item synchronously and emits the result directly, flatMap transforms each item into a new Observable and merges all of those inner Observables into the output stream, which is what makes it the right tool for chaining asynchronous calls.

apiUserObservable
    .map(apiUser -> apiUser.toUser())               // sync transform, User out for every ApiUser in
    .flatMap(user -> api.getUserPosts(user.id))       // async call per item, merges the results in
    .subscribe(posts -> render(posts));

map takes a function from T to R and applies it item by item, one value in, one value out, immediately. There's no room in map for the transform itself to be asynchronous, since it has to return a plain value on the spot. flatMap takes a function from T to Observable<R>, so the transform can kick off its own async work, like a second network call, and flatMap subscribes to each of those inner Observables and merges their emissions into a single output stream.

The tell in an interview is any transform that itself needs to make a network call or hit a database, that's flatMap, because only flatMap lets the mapping function return something that emits later instead of a value right now. The direct Flow equivalent is the same split, map for a synchronous transform, flatMapMerge when the transform is itself a suspend function or a Flow, though in Flow you'd often just call the suspend function directly with map since suspension doesn't need a separate operator the way an async callback did in RxJava.

When should you call dispose and clear on a CompositeDisposable in RxJava?

Tier: Less commonDifficulty: Medium

Call clear() when you want to cancel every current subscription but keep using the same CompositeDisposable afterward, call dispose() when you're completely done with it and it should reject any further subscriptions.

private val disposables = CompositeDisposable()

override fun onStop() {
    disposables.clear() // cancel subscriptions, but the container is still usable
}

override fun onDestroy() {
    disposables.dispose() // done for good, adding to it now would throw
}

clear() removes and disposes every Disposable it's currently holding, but leaves the CompositeDisposable itself in a usable state, so you can add new subscriptions to it afterward and it works exactly like before. dispose() does the same cleanup, but also flips the container's own state so that adding anything new to it after that point is silently dropped, or throws depending on the RxJava version.

That difference maps directly onto the Fragment lifecycle, clear() in onStop() or onPause(), since the same instance and its disposables field will be reused if the screen comes back, and dispose() in onDestroy(), since the view is gone for good at that point. Getting this backwards, calling dispose() too early, is a real bug, not just a style choice, any subscription you try to add afterward from a screen that's still alive just silently does nothing. Structured concurrency in coroutines sidesteps the whole question, cancelling a viewModelScope or lifecycleScope cancels every coroutine launched in it automatically, there's no separate container to manage or a wrong method to call on it.

When should you use the create operator and when the fromCallable operator in RxJava?

Tier: Less commonDifficulty: Medium

Use fromCallable when you're wrapping a single blocking call that returns one value, use create when you need to emit multiple items over time, or need to manage the subscription lifecycle yourself.

Observable<User> single = Observable.fromCallable(() -> api.getUser(id)); // one value, deferred until subscribe

Observable<Location> stream = Observable.create(emitter -> {
    LocationListener listener = location -> {
        if (!emitter.isDisposed()) emitter.onNext(location); // guard every emission
    };
    locationManager.requestUpdates(listener);
    emitter.setCancellable(() -> locationManager.removeUpdates(listener));
});

fromCallable takes a Callable, runs it lazily on subscription, and emits its single return value or propagates any exception it throws as onError. It's the simplest way to turn an existing synchronous, blocking function into an Observable, without writing any emitter logic yourself.

create gives you the raw ObservableEmitter and full control over when onNext, onError, and onComplete fire, which is what you need for a callback based API, like a location listener or a socket connection, that can push multiple events over an open ended period of time. It also puts the burden on you to check emitter.isDisposed() before every emission, since emitting after the subscriber has unsubscribed sends the error to RxJava's global error handler instead of anywhere you're actually listening for it, and to wire up setCancellable so the underlying listener actually gets torn down. callbackFlow is the direct Flow equivalent for the create case, offering the same emit and cleanup shape with awaitClose in place of setCancellable, while a plain suspend fun covers what fromCallable did, no wrapper needed.

When should you use the defer operator in RxJava?

Tier: Less commonDifficulty: Hard

Use defer whenever an Observable needs to capture fresh state at subscription time instead of at the moment it was created, since defer doesn't build the Observable at all until a subscriber actually subscribes, and builds a brand new one for each subscriber.

String carModel = "DEFAULT";

Observable<String> eager = Observable.just(carModel);          // captures "DEFAULT" right now
Observable<String> deferred = Observable.defer(() -> Observable.just(carModel)); // captures at subscribe time

carModel = "BMW";

eager.subscribe(System.out::println);     // prints DEFAULT, stale
deferred.subscribe(System.out::println);  // prints BMW, current

Observable.just(carModel) evaluates carModel immediately, when that line runs, and bakes that value into the Observable forever, so any later change to carModel is invisible to it. Observable.defer(() -> ...) instead takes a factory function and doesn't call it until someone subscribes, so by the time it runs, it sees whatever the current state actually is.

The two situations this actually matters are a value that can change between when the Observable is built and when it's subscribed to, and multiple subscribers who should each see their own fresh snapshot rather than sharing one value computed at construction time. In coroutines this problem mostly doesn't arise the same way, since a flow { } builder already runs its block fresh for every collector by default, defer's behavior is Flow's ordinary behavior, not a special operator you have to remember to reach for.