Handover notes for the Amro Android app, a TMDB-powered movie browser that shows trending films, supports filtering and sorting, and navigates to a movie detail screen.
- Trending screen: fetches daily trending movies from The Movie Database (TMDB) API, displays them in a scrollable list, and lets the user filter by genre and sort by popularity, title, or release date.
- Detail screen: shows extended information for a selected movie (overview, poster, genres, etc.).
- Navigation: single-activity Compose app; tapping a movie pushes a detail destination onto the back stack.
The app is structured as a modular Gradle project so that domain logic, networking, design tokens, and feature UI can evolve independently.
The project follows Clean Architecture with an MVI-style presentation layer.
| Layer | Module(s) | Role |
|---|---|---|
| Presentation | feature:trending, feature:detail |
Compose screens, UiState / Event types, ViewModels |
| Domain | domain |
Pure Kotlin — use cases, domain models, repository contracts |
| Data | core:network |
Ktor client, DTOs, mappers, repository implementations |
| Shared | core:common, core:common-android, core:designsystem |
Dispatchers, error helpers, theme, reusable components |
Each feature ViewModel exposes:
- A
StateFlow<UiState>: single source of truth for what the UI renders. - A sealed
Eventinterface: user actions are sent viaonEvent(...). - The ViewModel reacts to events by updating internal state; reactive flows (from use cases) are combined and mapped into
UiState.
Screens are stateless composables: they receive uiState and callbacks, which keeps them easy to preview, screenshot-test, and instrument-test without a ViewModel.
Dependencies always point inward: feature → domain ← core:network. The domain module has no Android dependency and runs on the JVM, which keeps business logic fast to unit test.
| Module | Type | Purpose |
|---|---|---|
:app |
Application | Entry point, Navigation 3 back stack, Koin bootstrap |
:domain |
JVM library | Use cases and repository interfaces |
:core:network |
Android library | TMDB API client and repository implementations |
:core:designsystem |
Android library | Material 3 theme, typography, shared components |
:core:common |
JVM library | DispatcherProvider, shared non-Android utilities |
:core:common-android |
Android library | Android-specific helpers (e.g. error string mapping) |
:feature:trending |
Android library | Trending screen, filter/sort UI |
:feature:detail |
Android library | Movie detail screen |
:test-data |
JVM library | Test stubs, coroutine test extension |
:test-data-android |
Android library | Shared Robot base class for UI tests |
- Compose (Material 3) for all UI. Screens are declarative and testable in isolation.
- Navigation 3 (
androidx.navigation3) manages a typed back stack of@SerializableNavKeydestinations (Trending,Detail(movieId)). Navigation lives in:app(AppNavDisplay.kt); feature modules stay unaware of routing. - Coil 3 loads poster images via
coil-composewith the OkHttp network fetcher, reusing the same HTTP stack philosophy as Ktor.
- Ktor client with OkHttp engine, content negotiation, and request logging.
- kotlinx.serialization for JSON DTOs. Unknown keys are ignored and input values are coerced to keep the client resilient to API changes.
- Repository implementations in
core:networkmap DTOs to domain models; the domain layer never sees wire format types.
Koin was chosen for lightweight, compile-time-friendly dependency injection without annotation processing. Modules are split per layer (domainModule, networkModule, trendingModule, detailModule) and composed in appModule. ViewModels are resolved in composables via koinViewModel().
Use cases and repositories expose Flow where data can change over time (e.g. trending movies). ViewModels use stateIn with SharingStarted.WhileSubscribed(5_000) to avoid work when the UI is not visible. A DispatcherProvider abstraction keeps dispatchers injectable and test-friendly.
| Tool | Usage |
|---|---|
| JUnit 5 | All JVM unit tests (useJUnitPlatform()) |
| MockK | Mock use cases and repositories |
| Turbine | Assert on Flow / StateFlow emissions in ViewModel tests |
| AssertJ | Fluent assertions (BDDAssertions.then(...)) |
The :test-data module provides:
CoroutineTestinterface +TestCoroutineExtension— sets upUnconfinedTestDispatcher, replacesDispatchers.Main, and injects a testDispatcherProvider.- Domain/DTO stubs (
DomainStubs,DtoStubs) for consistent test fixtures.
Domain use cases, repository implementations, and ViewModels all have unit test coverage.
Paparazzi runs Compose screenshot tests on the JVM (no emulator required). TrendingScreenScreenshotTest uses:
- TestParameterInjector to parameterise over light/dark theme and multiple named UI states (loading, content, error) in a single test method.
- Paparazzi with a Pixel 5 device config to capture golden images.
Run screenshot tests and refresh goldens:
./gradlew :feature:trending:recordPaparazzi
./gradlew :feature:trending:verifyPaparazziFeature-level instrumented tests live in feature:trending/src/androidTest. They use:
- Compose Test Rule (
createComposeRule) to set screen content directly with stubbedUiState— no network, no ViewModel. - Robot pattern —
TrendingRobotextendsBaseRobotfrom:test-data-android, encapsulating find/click/assert helpers. Tests read as scenarios:trendingRobot { assertMovieDisplayed("Inception") }. - AndroidX Test Orchestrator — configured via
testOptions.execution = "ANDROIDX_TEST_ORCHESTRATOR"withclearPackageDataanduseTestStorageServicefor isolated, clean test runs.
./gradlew :feature:trending:connectedDebugAndroidTestNote: Instrumented tests require a connected device or emulator. Espresso is on the classpath (standard AndroidX test setup) but the trending UI tests primarily use Compose Test APIs, which align better with Compose semantics.
Detekt runs at the root level with custom rules in config/detekt/detekt.yml. Notable config choices:
- Complexity rules relaxed for
@Composable, mappers, and preview data. detekt-formattingplugin applied for ktlint-compatible formatting checks.
./gradlew detektRequirements: Android Studio (recent), JDK 17+ (domain module targets JVM 21), Android SDK 37.
# Build debug APK
./gradlew :app:assembleDebug
# Run all unit tests + snapshot tests
./gradlew test
# Run instrumented tests
./gradlew cATTMDB access uses a Bearer token configured in NetworkClient. For production this should be moved to local.properties, BuildConfig, or a secrets manager, it is hard-coded for assignment convenience.
- Feature modules own their DI modules (
trendingModule,detailModule) but not navigation — keeps features reusable and navigation centralised in:app. - UiModels are separate from domain models: mapping happens in the feature layer (
toUiModel()), so presentation formatting (e.g."Pop: 123.4") does not leak into domain. - Screens accept state + callbacks: this was deliberate to maximise testability (Paparazzi, Compose Test, and
@Previewall work without a ViewModel). - Genre list is fetched independently from TMDB's genre endpoint rather than derived only from the current movie list, giving a stable filter chip set.
- Sort/filter logic lives in domain use cases (
FilterMoviesUseCase,SortMoviesUseCase,GetAvailableGenresUseCase) so it is unit-testable without Android or Compose. :test-data/:test-data-androidsplit: JVM test utilities stay separate from Android Robot helpers, matching the same boundary as:core:common/:core:common-android.
- Connection-specific error UI:
TrendingViewModelhas a commented TODO for distinguishing network errors from generic failures viaisConnectionError(). - Detail screen tests: coverage is concentrated on trending (the primary assignment surface); detail could follow the same Paparazzi / Robot patterns.
- Release optimisation: R8/minification is disabled in the release build type for easier debugging during development.
- Gradle buildSrc/build-logic: modules got the similar setup. By creating
buildSrcor plugins (build-logic) will speed up the next feature development.
Amro/
├── app/ # Application shell, navigation, Koin
├── domain/ # Use cases, models, repository interfaces
├── core/
│ ├── common/ # JVM shared utilities
│ ├── common-android/ # Android shared utilities
│ ├── designsystem/ # Theme and components
│ └── network/ # Ktor client, DTOs, repositories
├── feature/
│ ├── trending/ # Trending list feature
│ └── detail/ # Movie detail feature
├── test-data/ # JVM test fixtures and extensions
├── test-data-android/ # Robot base for instrumented tests
└── config/detekt/ # Detekt rule configuration
If you pick this up, start with AppNavDisplay.kt for navigation flow, TrendingViewModel.kt for the MVI pattern, and domain/ for business rules. The test modules are a good reference for how to add coverage to new features consistently.