•CASE STUDY

Android MVVM Architecture for API-Driven Screens

4 min read·791 words·Intermediate

Asked at

1 candidate report in May 2026

How to use this case study

SDE-2 / Mid

  • Explain the layers (View, ViewModel, Repository, data sources)
  • How a screen loads data from an API
  • How UI state is represented

SDE-3 / Senior

  • Go deeper on coroutines/Flow
  • Error handling and retries
  • Offline cache with Room (single source of truth)
  • Configuration changes and pagination

Staff / Principal

  • Discuss modularization and dependency injection
  • Testing strategy
  • API contract evolution between app versions

Problem RestatementProblem

Design the client-side architecture of an Android app screen that calls backend APIs, using MVVM (Model–View–ViewModel), as asked at OpenAI for a mobile role. For example: a list of conversations that loads from an API, supports pull-to-refresh, pagination and offline viewing, and survives screen rotation. Cover the HTTP request lifecycle, error handling, caching and testability.

The Layers

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    V["View - Activity / Compose UI"] -->|"user events"| VM["ViewModel - holds UiState"]
    VM -->|"StateFlow of UiState"| V
    VM --> UC["Use cases (optional)"]
    UC --> R["Repository - single source of truth"]
    R --> LOCAL[("Local DB - Room")]
    R --> REMOTE["Remote API - Retrofit / OkHttp"]
    REMOTE --> NET["Backend"]
  • View (Jetpack Compose or Activity/Fragment): draws the UI from state and sends user events (tap, refresh) to the ViewModel. No business logic.
  • ViewModel: holds the screen's UiState, survives configuration changes (rotation), runs work in viewModelScope, and exposes StateFlow<UiState>.
  • Repository: decides where data comes from (cache or network), and hides Retrofit and Room from the ViewModel.
  • Data sources: Retrofit API (remote) and Room database (local).
  • Dependency injection (Hilt) wires these together, which makes testing easy.

UI State

data class ConversationsUiState(
    val items: List<ConversationUi> = emptyList(),
    val isLoading: Boolean = false,
    val isRefreshing: Boolean = false,
    val error: String? = null,        // user-friendly message
    val endReached: Boolean = false
)

One immutable state object per screen. The View just renders it. This avoids bugs where the loading spinner and error message disagree.

Deep Dive — Where the request livesDeep dive

A screen loads conversations from an API. Where that call is owned decides what happens on rotation, on a slow network, and when the user leaves mid-request.

Weak

Call the API from the Activity

The Activity starts the request in onCreate and updates the views in the callback.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  A["Activity - starts the request"] --> ROT["User rotates the phone"]
  ROT --> DES["Activity destroyed and recreated"]
  DES --> RE["Request starts again from scratch"]
  DES --> LEAK["The old callback still holds the dead Activity"]
  LEAK --> CRASH["Updates a destroyed view - crash or leak"]

The request's lifetime is tied to a component Android destroys routinely. Every rotation refetches, and the in-flight callback outlives the thing it was going to update.

Good

Move it to a ViewModel with callbacks

A ViewModel survives configuration changes, so the request is not restarted and the callback has something valid to update.

The lifecycle problem is solved. The state problem is not: the screen ends up with isLoading, items, error and isRefreshing as separate fields, updated from different callbacks. They drift into combinations that should be impossible — a spinner over an error, stale items under a fresh error — and every new caller has to remember to reset all of them.

Best

One immutable state object, one source of truth

Expose the whole screen as a single immutable UiState in a StateFlow, and read the data from the database rather than from the network response:

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  NET["Network refresh"] --> ROOM[("Room - single source of truth")]
  ROOM -->|"Flow emits on change"| VM["ViewModel - updates one UiState"]
  VM --> UI["UI renders the state it is given"]
  UI -->|"rotation"| VM
  VM -->|"onCleared"| CANCEL["viewModelScope cancels in-flight work"]
  ROOM --> OFF["Offline: the same Flow serves cached data"]
  • One state object means the screen can only render combinations the ViewModel actually constructed. There is no set of fields to keep consistent, because there is only one value.
  • The database is the source of truth, not the response. The network writes to Room; the UI observes Room. Offline works for free, a partial refresh never blanks the list, and two screens showing the same data cannot disagree.
  • viewModelScope ties work to the screen's real lifetime. Leaving cancels in-flight requests automatically — no manual bookkeeping, no callback firing into a destroyed view.

The rule underneath all three: the UI renders state and emits events; it never decides anything. That is what makes the ViewModel testable without an emulator, and it is what the interviewer is checking for.

Networking Details

  • Retrofit + OkHttp: an interceptor adds the auth token, and an authenticator refreshes expired tokens once and retries.
  • Timeouts: connect/read timeouts (e.g., 10s/30s). For streaming responses (LLM tokens), use a streaming call and update the state incrementally.
  • Retries: automatic retry with backoff only for idempotent GETs and network errors. Never auto-retry non-idempotent POSTs without an idempotency key.
  • Error mapping: convert HTTP and IO errors into a Result.Error with a user-friendly message (no connection, session expired, server error) in the repository, so the ViewModel stays simple.
  • HTTP caching: ETags / If-None-Match to save data on refresh.

Pagination and Offline

  • Use Paging 3 with a RemoteMediator: it loads pages from the API into Room, and the UI pages from Room. Scrolling works offline for cached pages.
  • Queue user actions made offline (e.g., "archive conversation") with WorkManager, and retry when back online.

Testing

  • ViewModel tests: fake the repository, and assert UiState transitions (loading → items; refresh error → error message).
  • Repository tests: MockWebServer for the API, an in-memory Room DB.
  • UI tests: Compose testing with fake state.

Wrap-UpWrap-up

Keep the View dumb, give each screen a ViewModel that exposes a single immutable UiState through StateFlow and launches work in viewModelScope, and put a repository in front of Retrofit and Room with Room as the single source of truth. Handle auth, timeouts, safe retries and error mapping in the data layer, use Paging 3 with RemoteMediator for pagination and offline, and inject dependencies with Hilt so every layer is testable in isolation.

More Case Studies

Frequently Asked Questions

What is the Android MVVM Architecture for API-Driven Screens system design question?

Android MVVM Architecture for API-Driven Screens is a system design interview question asked at FAANG companies. It covers frontend, api design, caching and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Android MVVM Architecture for API-Driven Screens question?

OpenAI have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Android MVVM Architecture for API-Driven Screens interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Android MVVM Architecture for API-Driven Screens question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →