Breadth & Contrast (REDI-90…93): detail screen, MVVM about, Views renderer, Compose↔View interop
Android Architecture Showcase
A single runnable Android-only (Jetpack Compose) reference app that demonstrates good architecture conventions — each in its own module/example. Teaching repo: every module is meant to be minimal but complete and idiomatic.
Status: built milestone-by-milestone from the Linear backlog. Foundation, Core Infrastructure, the Flagship MVI characters feature, and Breadth & Contrast (character detail, the MVVM About screen, the Views renderer, and Compose↔View interop) are complete and the project assembles green. Full architecture docs land with the Quality & Docs milestone.
Stack
Multi-module Gradle + build-logic convention plugins · Koin (constructor DSL) · Ktor ·
KotlinX Serialization · Coil · Timber · type-safe Compose Navigation. Data comes from the no-key
Rick & Morty API.
What it showcases: MVI as the primary presentation pattern (flagship characters feature),
an MVVM contrast screen (about), and the same MVI ViewModel driven by two renderers —
Jetpack Compose and classic XML + ViewBinding + RecyclerView — proving the presentation logic is
UI-toolkit-agnostic. See Presentation patterns below.
Module structure
:app → wires everything; single Activity, Compose host
:build-logic → Gradle convention plugins (the only place versions/config live)
:core:domain → Result/error types, shared domain models (pure Kotlin)
:core:data → Ktor HttpClient factory, safe-call helpers
:core:presentation → UiText, ObserveAsEvents, DataError → UiText
:core:design-system → AppTheme + reusable composables
:feature:characters:domain → models + repository interface (pure Kotlin)
:feature:characters:data → DTOs, mappers, data source, repository impl
:feature:characters:presentation → MVI ViewModel/State/Action/Event (UI-agnostic: no Compose, no Views)
:feature:characters:presentation-compose → Compose renderer
:feature:characters:presentation-views → Views/XML renderer (same ViewModel)
:feature:about:presentation → MVVM contrast screen
Dependency rules: presentation → domain ← data; domain depends only on :core:domain;
features never depend on other features; :app wires the graph.
Presentation patterns (MVI vs MVVM)
Both patterns live side by side so the trade-off is concrete, not theoretical.
MVI (:feature:characters:*) |
MVVM (:feature:about:presentation) |
|
|---|---|---|
| State | one immutable State data class |
one immutable State data class |
| User input | a single onAction(Action) funnel + sealed Action |
plain public methods on the ViewModel |
| Side effects | one-time Events via a Channel (nav, snackbar) |
none — the screen calls a method / uses LocalUriHandler directly |
| Best when | state is complex and interacting; effects matter | the screen is small and mostly static |
The flagship characters list is MVI because its state is genuinely complex — pagination, loading
vs. next-page loading, error surfacing, and SavedStateHandle restore after process death — and it
emits navigation/snackbar effects. The About screen is deliberately MVVM: a StateFlow plus a couple
of public methods, with no Action and no Event types at all, because that ceremony would be
pure overhead for static content. Rule of thumb: reach for MVI when state is complex and side
effects matter; reach for MVVM when the screen is simple.
One ViewModel, two renderers
:feature:characters:presentation is UI-toolkit-agnostic — it has no Compose and no Views
dependency (state stays Compose-stable via kotlinx-collections-immutable rather than @Stable).
The exact same CharacterListViewModel (State/Action/Event/UI-model) is rendered twice:
:feature:characters:presentation-compose— Jetpack Compose (LazyColumn).:feature:characters:presentation-views—Fragment+ ViewBinding +RecyclerView/DiffUtil.
:app hosts the Views renderer inside the Compose NavHost via AndroidFragment (Compose↔View
interop) and injects all navigation as callbacks, so the renderers stay decoupled from each other.
Build & run
./gradlew assembleDebug # build the APK
./gradlew projects # print the module tree
./gradlew check # tests + lint (added in the Quality & Docs milestone)
Requires JDK 17+ (the Gradle build pins a Java 17 toolchain) and the Android SDK
(compileSdk 36, minSdk 24).