androidinterview.com

Android Architecture Interview Questions

19 questions

Tier
Difficulty
Level

Showing all 19 questions

Architecture Patterns

Expect to be asked to compare two of these, not to define one.

Describe MVVM.

Tier: EssentialDifficulty: Easy

MVVM splits an app into three layers, Model, View, and ViewModel, so the UI and the business logic don't end up tangled together in the same class.

The Model handles data and business logic, repositories, local and remote data sources, and plain data classes. The View is the UI layer, an Activity, Fragment, or composable, and its only job is to render whatever state it's given and forward user actions onward. The ViewModel sits between the two, it pulls data from the Model, turns it into UI ready state, and exposes that state through something observable like StateFlow or LiveData.

class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user

    fun loadUser(id: String) {
        viewModelScope.launch { _user.value = repo.getUser(id) }
    }
}

The key detail that makes MVVM work is that the ViewModel has no reference to the View at all, it doesn't hold an Activity, a Fragment, or a Context. The View observes the ViewModel and updates itself, but the ViewModel never reaches back to touch the View directly. That one way relationship is what makes the ViewModel testable on its own and what lets it survive configuration changes without dragging a dead View reference along with it.

Compare MVC vs MVP vs MVVM architecture.

Tier: EssentialDifficulty: Medium

All three split an app into a data layer and a UI layer, the difference is how much the UI layer knows about the other two, and how that plays out on Android specifically.

MVC is the oldest and the weakest fit for Android. In theory the Controller sits between Model and View, but on Android the Activity or Fragment usually ends up playing both Controller and View at once, since it both handles input and directly manipulates its own views. That makes it hard to test and easy to end up with a bloated Activity that does everything.

MVP fixes the testing problem by pulling the logic out into a Presenter, which talks to the Model and updates the View through a plain interface. The View, usually the Activity or Fragment, becomes a thin layer that just implements that interface and forwards user actions to the Presenter. The Presenter itself is unit testable since it only depends on an interface, not a real Activity, but it does hold a direct reference to the View, so you have to manage that reference carefully or you leak the View when it's destroyed while the Presenter is still doing work.

MVVM removes that reference entirely. The ViewModel exposes state through something observable, LiveData or StateFlow, and the View just subscribes to it, so the ViewModel never needs to know the View exists at all. That avoids the leak problem MVP has by construction, and on Android it pairs naturally with the ViewModel class, which already survives configuration changes for you. That combination is why MVVM is the default choice on Android today, MVP still works but needs more manual lifecycle discipline, and MVC mostly shows up as an anti pattern people are warned away from.

What is MVI (Model-View-Intent) architecture and how does it compare to MVVM?

Tier: CommonDifficulty: Medium

MVI structures a screen's state as a single, immutable object, and every user action becomes an Intent that gets reduced into a brand new state object, never a mutation of the old one. MVVM structures a screen through a ViewModel exposing state, usually as StateFlow or LiveData, but doesn't require that state to live in one object, a ViewModel commonly exposes several independent properties that each update on their own.

// MVI, one state object, one entry point for change
sealed interface Intent { data class Search(val query: String) : Intent }
data class UiState(val query: String = "", val results: List<Item> = emptyList())

fun reduce(state: UiState, intent: Intent): UiState = when (intent) {
    is Intent.Search -> state.copy(query = intent.query)
}

MVI is really MVVM with two extra constraints bolted on, a single state object instead of several loose ones, and a named Intent type instead of calling ViewModel functions directly. Both patterns are unidirectional, and both use an observable holder to push state to the View, MVI just makes the shape stricter, which pays off on screens with a lot of interacting state, a form with validation, loading, and error states all at once, where several independent properties can drift out of sync with each other in ways a single state object can't.

The cost is the same story as Clean Architecture, more boilerplate, sealed classes for every intent and every state, for a benefit that's easy to feel on a complex screen and hard to justify on a simple one. Plenty of teams get MVI's actual goal, predictable, one way state, out of a well disciplined MVVM ViewModel exposing a single StateFlow<UiState>, without adopting the full Intent vocabulary.

What is Unidirectional Data Flow (UDF)?

Tier: CommonDifficulty: Medium

Unidirectional Data Flow means state moves in exactly one direction, down from a single source of truth to the UI, and events move back up, from the UI to whatever owns the state. The UI never mutates state directly, it only renders whatever state it's given and reports what the user did.

data class UiState(val query: String = "", val results: List<Item> = emptyList())

class SearchViewModel : ViewModel() {
    private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state.asStateFlow()

    fun onQueryChanged(query: String) {                // event flows up
        _state.update { it.copy(query = query) }        // state flows back down
    }
}

The loop is always the same shape, an event goes up to the ViewModel, the ViewModel produces new state, the new state flows back down and the UI re-renders. There's no path where a Composable or a Fragment reaches back and edits the state object directly, which is what makes the state predictable, at any point you can look at one object and know exactly what the screen should show.

This is the same principle MVI names explicitly with its Model Intent View loop, and it's the default shape of state management in Jetpack Compose, where a Composable takes state as a parameter and emits events as lambdas, it never holds mutable state that the rest of the app can reach into.

Read more UI layer (opens in a new tab)

Why use MVVM over MVP? Could you have used observables with RxJava instead?

Tier: CommonDifficulty: HardAsked at: meesho

MVVM over MVP mainly comes down to lifecycle. A ViewModel has no reference to its View at all, it just exposes state, and it's scoped by the platform to survive configuration changes on its own. A Presenter in MVP holds a direct reference to the View, doesn't survive recreation, and has to be manually created and rebound every time the Activity or Fragment comes back, with that same View reference then needing careful nulling to avoid leaking it.

Yes, Observable from RxJava could have played the same role LiveData or StateFlow plays now, it's the same idea, an observable holder the View subscribes to. Plenty of production apps did exactly that for years before Kotlin coroutines and Jetpack matured. The reasons that combination lost ground, RxJava has a much bigger API surface to learn, dozens of operators and multiple stream types, Observable, Single, Flowable, and it brings in a large third party dependency for something coroutines and Flow now handle natively in Kotlin, with tighter integration into viewModelScope and structured concurrency.

The actual architectural win, MVVM over MVP, would hold regardless of which observable type backed it, RxJava's Observable, LiveData, or StateFlow. What changed the industry's choice is that Kotlin made StateFlow and coroutines the path with the least friction, not that RxJava couldn't have done the job.

Architecture Components

What is LiveData in Android?

Tier: EssentialDifficulty: Easy

LiveData is a lifecycle aware observable data holder, it wraps a value and notifies observers when that value changes, but only while the observer is in an active lifecycle state.

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user

    fun loadUser(id: String) {
        viewModelScope.launch { _user.value = repository.getUser(id) }
    }
}

// in the Fragment
viewModel.user.observe(viewLifecycleOwner) { user -> renderUser(user) }

The lifecycle awareness is the whole point. A plain observable would keep calling back into a Fragment or Activity even after it's been destroyed, which is a common source of crashes and memory leaks. LiveData checks the state of the LifecycleOwner you pass to observe(), STARTED or RESUMED counts as active, and it automatically stops delivering updates once that owner drops below that, and unsubscribes entirely once it's destroyed.

It's typically exposed from a ViewModel as read only LiveData, backed by a private MutableLiveData the ViewModel updates internally, which is the same pattern you'd use with StateFlow. Google's own guidance has shifted toward Kotlin Flow for new code, but LiveData is still common in existing codebases and interviewers still expect you to know how it behaves.

What is the difference between setValue and postValue in LiveData?

Tier: EssentialDifficulty: Easy

setValue must be called from the main thread, and it updates the value, and notifies observers, immediately, synchronously. postValue can be called from any thread, background work included, it posts the update to the main thread and the actual write happens slightly later, asynchronously.

liveData.value = user           // setValue, main thread only
liveData.postValue(user)        // postValue, safe from a background thread

The gotcha worth knowing, if you call postValue multiple times before the main thread gets a chance to process any of them, only the last value survives, the earlier ones are simply overwritten and never delivered. That's fine for something like a progress percentage where only the latest matters, but it's a real bug if you're relying on postValue to deliver every intermediate value in order.

The rule of thumb, use setValue when you're already on the main thread, which is the common case inside a ViewModel using viewModelScope, since Dispatchers.Main is the default there. Reach for postValue only when you're genuinely updating from a background thread and don't want to hop back to the main thread yourself.

Read more LiveData overview (opens in a new tab)

What are the Android Architecture Components?

Tier: CommonDifficulty: Easy

Architecture Components are the Jetpack libraries built specifically to help structure an app around lifecycle awareness and a clear separation between UI and data.

  • ViewModel holds UI state and survives configuration changes, it's the anchor most of the others are built around.
  • LiveData, or StateFlow in newer code, is the observable holder a ViewModel exposes state through.
  • Lifecycle gives components a standard way to react to an Activity or Fragment's state, it's what makes LiveData and repeatOnLifecycle lifecycle aware in the first place.
  • Room is the abstraction over SQLite, with compile time checked queries and native Flow support.
  • Navigation handles fragment transactions and back stack management through a single graph instead of manual FragmentTransaction calls.
  • Paging loads large datasets from a database or network a page at a time instead of all at once.
  • WorkManager schedules deferrable, guaranteed background work that should still run even if the app is killed or the device reboots.

They're grouped together because they're designed to be used together, a ViewModel exposing a Room query as Flow, displayed through a Paging list, is the standard shape of a data heavy Android screen today. Knowing the list matters less than knowing which one solves which problem, that's usually the actual follow up question.

What is Android Jetpack and why should you use it?

Tier: CommonDifficulty: Easy

Jetpack is Google's umbrella collection of libraries for the plumbing that comes up in nearly every Android app, lifecycle management, navigation, background work, database access. It's not one library, it's a set of them, unbundled from the OS version, so you get updates through Gradle instead of waiting for users to update Android itself.

  • Lifecycle and ViewModel handle configuration changes and state without you writing manual save and restore logic.
  • Room replaces raw SQLite with compile time checked queries.
  • Navigation replaces manual FragmentTransaction calls with a graph.
  • WorkManager handles deferrable background work with OS version differences abstracted away.
  • Compose is the modern UI toolkit built on top of the rest of Jetpack.

The reason to use it over rolling your own, most of these problems, surviving a rotation without leaking a callback, running background work reliably across API levels, are solved problems with sharp edges if you get them wrong. Jetpack is Google's own answer to those edges, tested against a huge range of devices, and it's what the rest of the ecosystem, tutorials, other libraries, assumes you're using. Building your own version of ViewModel or WorkManager today would mostly be reinventing something already solved, not solving a new problem.

Clean Architecture & Layers

Explain Clean Architecture.

Tier: CommonDifficulty: Medium

Clean Architecture separates an app into layers arranged so dependencies only point inward, the business logic never depends on a framework, a database, or a UI toolkit, only on interfaces it defines itself.

  • The presentation layer, Activities, Fragments, ViewModels, depends on the domain layer.
  • The domain layer, use cases and plain Kotlin models, depends on nothing Android specific at all, it's pure business logic.
  • The data layer, repositories and data sources, implements the interfaces the domain layer defines, and depends inward on the domain layer rather than the other way around.
// domain layer, no Android import in sight
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: String): User = repository.getUser(id)
}

The problem it solves is testing and swapping implementations. The domain layer can be unit tested with no Android dependencies at all, and swapping Retrofit for a different network library, or Room for a different database, only touches the data layer, the use cases never notice.

The cost is real too, worth saying plainly. A use case that wraps a single repository call, GetUserUseCase calling repository.getUser(id) and nothing else, is indirection with no payoff on a small app. Clean Architecture earns its keep once there's enough business logic to actually test in isolation, or enough churn in the data layer to make the abstraction pay for itself. On a five screen app it's usually over engineering, on a codebase with real business rules and multiple data sources, it's the difference between a testable app and one where every test needs a database and a mock server.

What is Clean Architecture in the context of MVVM?

Tier: CommonDifficulty: Medium

In an MVVM app, Clean Architecture slots a domain layer between the ViewModel and the repository. Instead of the ViewModel calling repository.getUser(id) directly, it calls a use case, getUser(id), and the use case is the thing that actually depends on the repository interface.

class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: String) = repository.getUser(id)
}

class UserViewModel(private val getUser: GetUserUseCase) : ViewModel() {
    fun loadUser(id: String) {
        viewModelScope.launch { _user.value = getUser(id) }
    }
}

MVVM alone only guarantees the View and the ViewModel are decoupled, the ViewModel is still free to talk to a repository, or worse, a Retrofit service, directly. Adding the Clean Architecture domain layer on top means the ViewModel only ever depends on use case interfaces with no Android or networking types in them, which is what makes the business logic testable without touching a database or a mock server, and swappable if the data layer changes underneath it.

The honest cost, one use case class per operation is a lot of ceremony for a call that just forwards straight to a repository. It's worth doing once a use case actually contains logic, combining two repositories, applying validation, caching a decision, not as a rule applied to every ViewModel method regardless of whether there's anything to abstract.

Modularization

These turn up in senior rounds, usually attached to build times or to how a large team works in one codebase.

Multi-module project: why and when?

Tier: CommonDifficulty: Medium

The why is faster builds and enforced boundaries, the when is the part people get wrong. Modularizing on day one, before there's a real problem to solve, mostly adds Gradle boilerplate and interface indirection with nothing to show for it yet.

The signals worth modularizing for.

  • Clean build times creeping past a minute or two, since Gradle can build independent modules in parallel and skip ones that haven't changed.
  • More than one team working in the same codebase, stepping on each other's files or merging into the same handful of classes every week.
  • A piece of code, a design system, a networking layer, that's genuinely reused across more than one app or feature and needs a boundary enforced by the compiler, not just convention.

None of those show up in a five screen app built by two people. A reasonable path is to start as a single :app module with clean internal package boundaries, and split out modules once one of those signals actually shows up, :core-network when a second app needs it, :feature-x when a second team needs to own it without touching the rest. Modularizing early on a guess is the same mistake as adding Clean Architecture's domain layer before it's needed, effort spent on a problem the app doesn't have yet.

Read more Guide to Android app modularization (opens in a new tab)

What are the benefits of a multi-module architecture, and why use it?

Tier: CommonDifficulty: MediumAsked at: meesho

Splitting an app into Gradle modules mainly buys you two things, faster builds and enforced boundaries, at the cost of more setup and more indirection.

  • Faster builds, Gradle only recompiles the modules that actually changed, and can build unrelated modules in parallel, which matters once a project is big enough that a single module clean build takes minutes.
  • Enforced boundaries, a feature module simply cannot reach into another feature module's internals, because there's no dependency edge between them to exploit. That's a rule the compiler enforces, not just a convention in a style guide.
  • Parallel team ownership, different teams can own different modules and ship changes without stepping on each other's code as often.
  • Reusability, a module like :core-network or a design system module can be pulled into more than one app.

The honest tradeoff, on a small app with one or two developers, none of this pays for itself. The extra Gradle files, the interface indirection between modules, and the discipline of not reaching across module boundaries are all real costs. It starts paying off once build times get painful, or once enough people are working in the same codebase that merge conflicts and accidental coupling become the bigger problem.

How do you handle dependencies or abstractions in a multi-module project?

Tier: CommonDifficulty: HardAsked at: meesho

Modules depend on abstractions, not on each other's concrete implementations. A :core or :domain module defines the interfaces, UserRepository, AuthRepository, and both a :data module and any :feature module that needs them depend on that shared module, never on one another directly.

:app              -> depends on all feature modules, wires everything with Hilt
:feature-profile  -> depends on :domain, not on :feature-settings
:domain           -> defines interfaces, no Android dependencies
:data             -> implements :domain interfaces, depends on :domain

Two feature modules should never depend on each other directly, if :feature-profile needs something from :feature-settings, that shared piece belongs in a lower level module both can depend on instead. That rule is what keeps modules buildable and testable in isolation, and it's what Hilt's module system is built around, each feature module can declare its own Hilt bindings for the interfaces it needs, without knowing which concrete implementation actually gets wired in at the app level.

The tradeoff worth being honest about, this structure adds real friction, an extra interface and an extra module for something that could have been one class. It pays off once build times or team size make module boundaries worth enforcing, on a small app it's usually not worth setting up before you actually need it.

Less common, worth knowing

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

Architecture Patterns

Give a brief about Android architecture.

Tier: Less commonDifficulty: Easy

Android architecture is the layering that keeps the UI, the business logic, and the data separate, so a change to one doesn't force a rewrite of the others.

  • The UI layer, an Activity, Fragment, or composable, renders state and forwards user actions, it holds no logic of its own.
  • A ViewModel or presentation layer holds UI state and survives configuration changes, it talks to the layer below through interfaces, not concrete classes.
  • A data layer, repositories wrapping network and database sources, is the single source of truth the ViewModel pulls from.

Most production apps today build this as MVVM with a repository pattern, StateFlow or LiveData exposing state from the ViewModel, and Hilt wiring the layers together. Whether you add a full domain layer with use cases is Clean Architecture territory, and it's worth adding once the app has enough business logic to justify the extra indirection, not by default. On a small app, three layers, UI, ViewModel, repository, is usually the right amount, not five.

Read more Guide to app architecture (opens in a new tab)

Is there any issue with the Presenter in MVP?

Tier: Less commonDifficulty: Medium

The Presenter in MVP holds a direct reference to the View, through an interface, which sounds like it decouples them, but the Presenter itself has no lifecycle awareness and doesn't survive a configuration change. When the Activity is destroyed and recreated on rotation, the old Presenter, and whatever state it was holding, is gone, and a new one has to be created and rebound to the new View instance from scratch.

interface UserView {
    fun showUser(user: User)
}

class UserPresenter(private var view: UserView?) {
    fun loadUser(id: String) {
        view?.showUser(repository.getUser(id)) // view reference must be nulled on detach
    }
}

That reference to the View is also the other half of the problem, it has to be nulled out in onDestroy or onDetachedFromWindow, or the Presenter leaks the Activity for as long as an async call is still in flight. Miss that cleanup and a background callback resolving after the screen is gone crashes on a stale View reference or leaks memory.

This is the exact pair of problems MVVM was built to solve. A ViewModel has no reference to its View at all, it exposes state the View observes, and it's scoped to survive configuration changes by construction rather than by manual rebinding, which removes both failure modes at once.

What is the difference between software architecture and software design?

Tier: Less commonDifficulty: Medium

Software architecture covers the high level decisions, how the app splits into layers or modules, which pattern like MVVM or Clean Architecture it follows, how data flows between screens. These are the decisions that are expensive to change once a team has built on top of them.

Software design covers the lower level decisions inside a single class or module, which design pattern fits a specific problem, how a function is structured, what a class's public API looks like. These are decisions you can usually rewrite in an afternoon without touching the rest of the app.

A useful way to tell them apart, ask what it costs to change your mind. Swapping MVP for MVVM across an app is an architecture change, it touches every screen. Swapping a Singleton for a Factory inside one repository is a design change, it's contained to that file. Interviewers ask this to see if you can reason about scope, not just recite definitions, the same decision can be architecture in a small app and just design in a large one, depending on how far its blast radius reaches.

Architecture Components

How is LiveData different from ObservableField?

Tier: Less commonDifficulty: Medium

LiveData is lifecycle aware, it stops delivering updates to a destroyed or stopped observer automatically. ObservableField, part of the Data Binding library, has no concept of a lifecycle at all, it just holds a value and notifies whatever is bound to it whenever that value changes, regardless of whether the observing view is even visible.

val name = ObservableField<String>("")           // Data Binding only, no lifecycle awareness
val name: LiveData<String> = MutableLiveData("") // works anywhere, lifecycle aware

ObservableField only works inside the Data Binding system, bound directly to a view in XML, it isn't something you observe from a plain Kotlin class or a unit test the way you can with LiveData. LiveData is also strongly typed with generics from the start, ObservableField<T> needed a typed subclass for a while before it caught up.

In practice this comparison is mostly historical. Data Binding itself has fallen out of favor as Compose has taken over, and ObservableField was always the narrower, more coupled tool of the two even when both were common.

What is Android Data Binding?

Tier: Less commonDifficulty: Medium

Data Binding is a Jetpack library that binds views in an XML layout directly to data in code, generating a binding class so you can skip findViewById and manual update calls.

<TextView
    android:text="@{viewModel.userName}" />
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.viewModel = viewModel
binding.lifecycleOwner = this

Once the binding class knows about a LifecycleOwner, and the data is exposed as LiveData or an observable field, the view updates itself whenever the underlying value changes, with no explicit observe call needed in the Activity. It also lets you call methods and reference resources straight from the XML with binding expressions, like android:onClick="@{() -> viewModel.onButtonClicked()}".

The tradeoff is that logic creeps into XML, binding expressions are easy to write and hard to test, hard to step through in a debugger, and hard to review compared to the same line in Kotlin. Most Compose based codebases have moved away from Data Binding entirely, since Compose reads state directly with no separate binding layer, and it's mostly relevant now for apps still on a View based UI.