androidengineers.Book a session

Kotlin architecture for model calls

Kotlin architecture for model calls

article35–50 min

Learn through PocketCards, the runnable flashcard app in Android + AI Cookbook. Open the hands-on codelab · Browse the pinned source.

Follow ownership, not a folder diagram

PocketCards uses one app module. Compose renders state and sends actions to PocketCardsViewModel. The ViewModel coordinates CardGenerator and DeckRepository through constructor injection. FirebaseCardGenerator knows the SDK; FileDeckRepository knows atomic disk writes. This is enough separation to substitute a fake generator without adding pass-through use cases or a framework for every class.

Compose → PocketCardsViewModel → CardGenerator → Firebase AI Logic
                             → DeckRepository → AtomicFile

The workflow ViewModel is activity-scoped because notes and draft cards span Create and Editor. It exposes a read-only StateFlow. This scope survives ordinary configuration changes, but unsaved drafts are not persisted across process death. Saved decks reload from disk. Naming those limitations is part of architecture, not an afterthought.

A request has a lifecycle

A generation action checks availability and input, marks generating true and launches work in viewModelScope. The UI disables duplicate generation. A successful response is validated before becoming an editable draft; failures preserve the notes. A request identity guard prevents a cancelled or superseded operation from publishing into a newer workflow.

// Excerpt: invalidating a request matters as well as cancelling its job.
fun cancel() {
    request++
    generation?.cancel()
    mutable.update { it.copy(generating = false) }
}

Cancellation is control flow. In generate, TimeoutCancellationException is handled before CancellationException so the explicit 45-second request timeout gets a useful message, while ordinary cancellation is rethrown. A broad catch that turns every cancellation into an error can resurrect a screen or display a false failure after the user leaves.

Durability changes the order of actions

The review feature persists favorites and dates with the card. updateReview sets studying while the save is in flight, ignores duplicate actions and calls its completion callback only after repository.save succeeds. If saving fails, the same card remains visible and the user can retry. Advancing first and saving later would show progress that might never exist on disk.

AtomicFile protects replacement of one JSON file from a partial write. The repository's mutex serializes access within the instance. This is a small-collection design, not a general replacement for a queryable database. Larger libraries, migrations and synchronization would require revisiting the storage contract.

Learn from the last-card bug

The original lazy layout read a mutable index after the completion decision had been made elsewhere. Finishing a five-card deck produced index 5, which is outside the valid range 0–4. The current screen captures one safe snapshot before composing its branches:

val cardIndex = queue.getOrNull(position)
val card = cardIndex?.let { deck.cards.getOrNull(it) }
// Render the card when present; otherwise render completion or an empty filter.

Checkpoint: Explain why bounds checks alone do not replace correct state ownership. Find the test that finishes all five cards, restarts and checks Due. Then simulate a failed rating save and prove that the screen does not advance.

Work through the race instead of memorizing cancellation

Imagine request A begins, the user leaves, then starts request B. A response arrives from A after B starts. Even if ordinary coroutine cancellation works, the state policy must reject obsolete work rather than rely on completion order. The sample invalidates the old request identity; read the matching checks in the ViewModel before predicting the final state.

EventCorrect visible result
A startsGeneration is in progress
A is cancelledIts workflow stops waiting
B startsB owns the current loading state
A finishes lateA must not replace B's draft or reset B's state
B succeedsB's validated cards become the editable draft

A second ordering problem appears when rating a card. Think of persistence as the commit point:

Tap Got it → mark save in flight → save updated deck
  success → publish saved state → advance
  failure → show error → retain current card for retry

This is a conceptual sequence, not a new function to paste into the project. The UI's disabled state helps communicate work, while the ViewModel guard protects against repeated actions. Test the guard independently of how quickly a user can tap.

Exit check: An activity-scoped ViewModel survives rotation. Does that prove an unsaved generated draft survives process termination? No: inspect durable storage. This baseline saves decks, not every transient editing state. State scope and disk persistence answer different questions.

Source and further reading

YOUR LEARNING JOURNEY

0 of 17 available lessons completed

Progress saved in this browser. No account needed.
Kotlin architecture for model calls | AI for Android Developers — Fundamentals | Android Engineers