|
|
|
|
@@ -8,11 +8,10 @@ Data comes from the no-key [Rick & Morty API](https://rickandmortyapi.com/). The
|
|
|
|
|
characters, opens a detail screen, renders that same list **twice** (Compose and classic Views), has
|
|
|
|
|
a small MVVM *About* screen for contrast, and a dedicated **error-handling demo**.
|
|
|
|
|
|
|
|
|
|
> **Status:** built milestone-by-milestone from the
|
|
|
|
|
> [Linear backlog](https://linear.app/adrian-kuta/project/android-architecture-showcase-b5ecdeddda6c).
|
|
|
|
|
> Foundation, Core Infrastructure, the flagship MVI feature, Breadth & Contrast, and Quality & Docs
|
|
|
|
|
> are complete; the project assembles green and ships unit + UI tests. The only optional item left is
|
|
|
|
|
> the Room offline-cache stretch (see [Optional: Room stretch](#optional-room-stretch)).
|
|
|
|
|
> **Status:** built milestone-by-milestone. Foundation, Core Infrastructure, the flagship MVI
|
|
|
|
|
> feature, Breadth & Contrast, and Quality & Docs are complete; the project assembles green and ships
|
|
|
|
|
> unit + UI tests. The only optional item left is the Room offline-cache stretch (see
|
|
|
|
|
> [Optional: Room stretch](#optional-room-stretch)).
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -29,7 +28,6 @@ a small MVVM *About* screen for contrast, and a dedicated **error-handling demo*
|
|
|
|
|
- [Testing](#testing)
|
|
|
|
|
- [Build & run (`android` CLI)](#build--run-android-cli)
|
|
|
|
|
- [Optional: Room stretch](#optional-room-stretch)
|
|
|
|
|
- [Convention skills index](#convention-skills-index)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -46,7 +44,7 @@ a small MVVM *About* screen for contrast, and a dedicated **error-handling demo*
|
|
|
|
|
| Navigation | type-safe Compose Navigation (`@Serializable` routes) |
|
|
|
|
|
| Logging | Timber |
|
|
|
|
|
| Async | Coroutines + Flow |
|
|
|
|
|
| Testing | JUnit 5, Turbine, AssertK, `kotlinx-coroutines-test`, Ktor `MockEngine`, Compose UI test |
|
|
|
|
|
| Testing | JUnit 5, MockK, Turbine, AssertK, `kotlinx-coroutines-test`, Ktor `MockEngine`, Compose UI test |
|
|
|
|
|
|
|
|
|
|
> **AGP 9 gotcha:** AGP 9.0 has **built-in Kotlin**. Applying `com.android.application`/`library`
|
|
|
|
|
> auto-applies the Kotlin Android plugin, so the convention plugins must **not** apply
|
|
|
|
|
@@ -90,8 +88,6 @@ A key consequence: `:core:presentation`'s `UiText` is **Compose-free**, and the
|
|
|
|
|
uses `implementation` (not `api`), so the UI-agnostic `:feature:characters:presentation` never gets
|
|
|
|
|
Compose on its classpath — which is what lets two different renderers share one ViewModel.
|
|
|
|
|
|
|
|
|
|
See **android-module-structure**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## The data → UI flow
|
|
|
|
|
@@ -118,10 +114,9 @@ CharacterListScreen / CharacterListFragment (:presentation-compose / -views)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- **DTOs** (`*Dto`) live in `data`; **domain models** are separate and never become DTOs/entities.
|
|
|
|
|
Mappers are pure extension functions in a `mappers/` package (`toDomain()`). See
|
|
|
|
|
**android-data-layer**, **android-data-layer-mappers**.
|
|
|
|
|
Mappers are pure extension functions in a `mappers/` package (`toDomain()`).
|
|
|
|
|
- **UI models** (`*Ui`) live in `presentation` and carry display-ready data (e.g. blank detail fields
|
|
|
|
|
pre-formatted to an em dash). See **android-presentation-mvi**.
|
|
|
|
|
pre-formatted to an em dash).
|
|
|
|
|
|
|
|
|
|
### Note — when to add a UseCase
|
|
|
|
|
|
|
|
|
|
@@ -133,7 +128,7 @@ rule it illustrates:
|
|
|
|
|
> would merely forward a single repository call, injecting the repository directly is fine.
|
|
|
|
|
|
|
|
|
|
Here the list VM uses the UseCase; the detail VM calls `CharacterRepository` directly — both are
|
|
|
|
|
correct, and the contrast is the point. See **android-module-structure**, **android-di-koin**.
|
|
|
|
|
correct, and the contrast is the point.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -167,8 +162,6 @@ Views side with `repeatOnLifecycle`.
|
|
|
|
|
Reach for MVI when state is complex **and** side effects matter. Reach for plain MVVM when the screen
|
|
|
|
|
is small, mostly static, and has no real side effects — the *About* screen is the canonical case.
|
|
|
|
|
|
|
|
|
|
See **android-presentation-mvi**, **android-compose-ui**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## One ViewModel, two renderers (Compose vs Views)
|
|
|
|
|
@@ -191,8 +184,6 @@ from navigation.
|
|
|
|
|
> the classic Views (e.g. `MaterialToolbar`, `?attr/colorOnSurfaceVariant`) require. A plain
|
|
|
|
|
> `ComponentActivity` or a non-Material theme breaks the Fragment renderer.
|
|
|
|
|
|
|
|
|
|
See **android-compose-ui**, **android-module-structure**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Errors: `Result`, `DataError`, `UiText`
|
|
|
|
|
@@ -230,8 +221,6 @@ Three distinct cases (`NO_INTERNET`, `NOT_FOUND`, `SERVER_ERROR`) each render th
|
|
|
|
|
**Retry** re-issues the last request as an Action; a successful load **clears** the error. The same
|
|
|
|
|
`ErrorState` + retry Action is what the real list and detail screens use.
|
|
|
|
|
|
|
|
|
|
See **android-error-handling**, **android-presentation-mvi**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Navigation
|
|
|
|
|
@@ -255,8 +244,6 @@ in `:app`.
|
|
|
|
|
UI-agnostic `presentation` module needs **no** navigation dependency. The same `SavedStateHandle`
|
|
|
|
|
also persists the list's loaded page across process death.
|
|
|
|
|
|
|
|
|
|
See **android-navigation**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Dependency injection (Koin)
|
|
|
|
|
@@ -285,26 +272,24 @@ The lambda form (`single { … }`) appears only where a constructor reference ca
|
|
|
|
|
not a constructor). Compose roots inject with `koinViewModel()`; the Fragment uses `by viewModel()` —
|
|
|
|
|
both resolve the **same** `CharacterListViewModel` class and supply its `SavedStateHandle`.
|
|
|
|
|
|
|
|
|
|
See **android-di-koin**, **koin-constructor-dsl**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
Tests prove the architecture, not just the code. Stack: **JUnit 5**, **Turbine** (Flow), **AssertK**,
|
|
|
|
|
`kotlinx-coroutines-test`, Ktor **`MockEngine`**, and Compose UI test.
|
|
|
|
|
Tests prove the architecture, not just the code. Stack: **JUnit 5**, **MockK**, **Turbine** (Flow),
|
|
|
|
|
**AssertK**, `kotlinx-coroutines-test`, Ktor **`MockEngine`**, and Compose UI test.
|
|
|
|
|
|
|
|
|
|
| What | Where | Kind |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `GetCharactersPageUseCase` | `:feature:characters:domain` `src/test` | pure JVM, JUnit 5 |
|
|
|
|
|
| `CharacterListViewModel`, `CharacterDetailViewModel` | `:feature:characters:presentation` `src/test` | JVM unit, fakes + Turbine + `SavedStateHandle` |
|
|
|
|
|
| `CharacterListViewModel`, `CharacterDetailViewModel` | `:feature:characters:presentation` `src/test` | JVM unit, MockK + Turbine + `SavedStateHandle` |
|
|
|
|
|
| `NetworkCharacterRepository` | `:feature:characters:data` `src/test` | JVM unit, Ktor `MockEngine` |
|
|
|
|
|
| `CharacterListScreen` (robot) | `:feature:characters:presentation-compose` `src/androidTest` | instrumented Compose UI |
|
|
|
|
|
|
|
|
|
|
Conventions demonstrated:
|
|
|
|
|
|
|
|
|
|
- **Fakes, not mocks.** `FakeCharacterRepository` is a real in-memory implementation with a
|
|
|
|
|
`failWith` toggle and call counts — tests assert against working behaviour, not recorded calls.
|
|
|
|
|
- **MockK for collaborators.** The ViewModel/UseCase tests stub the `CharacterRepository` interface
|
|
|
|
|
with MockK — `coEvery` scripts the suspend calls, `coVerify` asserts the paging/retry interactions.
|
|
|
|
|
- **VM tested through its public MVI surface** (State/Action/Event) with a directly-constructed
|
|
|
|
|
`SavedStateHandle`, so the same tests hold for either renderer. Coverage includes happy path,
|
|
|
|
|
error → `UiText` + snackbar `Event`, pagination end-reached, **process-death restore**, and the
|
|
|
|
|
@@ -330,8 +315,6 @@ runs on a device/emulator via `./gradlew :feature:characters:presentation-compos
|
|
|
|
|
(CI compiles it via `assembleDebugAndroidTest`). An Espresso test for the Fragment renderer is
|
|
|
|
|
possible but intentionally omitted (the VM logic is already covered by the shared unit tests).
|
|
|
|
|
|
|
|
|
|
See **android-testing**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Build & run (`android` CLI)
|
|
|
|
|
@@ -362,27 +345,9 @@ Requires JDK 17+ (the Gradle build pins a Java 17 toolchain) and the Android SDK
|
|
|
|
|
|
|
|
|
|
## Optional: Room stretch
|
|
|
|
|
|
|
|
|
|
Out of core scope and **not implemented** (tracked as the optional REDI-99). It would add a `room`
|
|
|
|
|
Out of core scope and **not implemented** (an optional stretch). It would add a `room`
|
|
|
|
|
convention plugin and a `:core:database` (or feature Room set) with `CharacterEntity` + DAO +
|
|
|
|
|
`@Database` (prefer `autoMigrations`), then convert the repository to **offline-first**
|
|
|
|
|
(`OfflineFirstCharacterRepository`: network → persist → expose a DB `Flow`; the ViewModel observes the
|
|
|
|
|
DB, never the network response). The current `CharacterRepository` returning the `DataError`
|
|
|
|
|
supertype already anticipates a multi-source implementation. See **android-data-layer**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Convention skills index
|
|
|
|
|
|
|
|
|
|
This repo is a narrative index of these conventions:
|
|
|
|
|
|
|
|
|
|
| Skill | Where it shows up |
|
|
|
|
|
|---|---|
|
|
|
|
|
| android-module-structure | module graph, dependency rules, convention plugins |
|
|
|
|
|
| android-presentation-mvi | characters list/detail/error-demo (State/Action/Event/VM) |
|
|
|
|
|
| android-compose-ui | Compose renderers, design-system, previews, stability |
|
|
|
|
|
| android-navigation | type-safe routes, per-feature graphs, callback decoupling |
|
|
|
|
|
| android-di-koin / koin-constructor-dsl | feature Koin modules, `*Of` constructor DSL |
|
|
|
|
|
| android-data-layer / android-data-layer-mappers | data sources, repository, DTOs, mappers |
|
|
|
|
|
| android-error-handling | `Result`/`DataError`/`UiText`, `safeCall`, the error demo |
|
|
|
|
|
| android-testing | unit tests, fakes, `MockEngine`, the robot UI test |
|
|
|
|
|
| android-cli | build/run/emulator steps above |
|
|
|
|
|
supertype already anticipates a multi-source implementation.
|
|
|
|
|
|