diff --git a/README.md b/README.md index 8d6c5fb..febd59f 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,21 @@ be minimal but complete and idiomatic. > **Status:** built milestone-by-milestone from the > [Linear backlog](https://linear.app/adrian-kuta/project/android-architecture-showcase-b5ecdeddda6c). -> **Foundation** (scaffold, version catalog, `build-logic` convention plugins) is complete and the -> project assembles green. Full architecture docs land with the *Quality & Docs* milestone. +> **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 · Kermit · type-safe Compose Navigation. Data comes from the no-key +KotlinX Serialization · Coil · Timber · type-safe Compose Navigation. Data comes from the no-key [Rick & Morty API](https://rickandmortyapi.com/). -What it will showcase: **MVI** as the primary presentation pattern (flagship *characters* feature), -an **MVVM** contrast screen, and the same MVI `ViewModel` driven by **two renderers** — Jetpack -Compose and classic **XML + ViewBinding + RecyclerView** — proving the presentation logic is -UI-toolkit-agnostic. +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](#presentation-patterns-mvi-vs-mvvm) below. ## Module structure @@ -40,6 +42,36 @@ UI-toolkit-agnostic. **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 `Event`s 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 ```bash