Adrian Kuta d232757eb4 REDI-96: repository MockEngine test + Compose robot UI test + serialization fix
NetworkCharacterRepositoryTest swaps a Ktor MockEngine into HttpClientFactory and
covers success mapping (incl. request URL/page-param construction), 404 ->
NOT_FOUND, 500 -> SERVER_ERROR, and malformed body -> SERIALIZATION. That last
case exposed a real bug: Ktor wraps the kotlinx SerializationException in its own
ContentConvertException, so safeCall mapped it to UNKNOWN; safeCall now scans the
cause chain and maps it to SERIALIZATION. Adds an instrumented Compose UI test
(CharacterListScreen) using the chaining CharacterListRobot: rendered items,
empty/error states, and tap -> Action.
2026-06-10 15:00:54 +02:00

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-viewsFragment + 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).

Description
No description provided
Readme 466 KiB
Languages
Kotlin 100%