Skip to content

WingEraser/amro

Repository files navigation

Amro

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.


What this app does

  • 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.


Architecture

The project follows Clean Architecture with an MVI-style presentation layer.

Layer responsibilities

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

MVI in practice

Each feature ViewModel exposes:

  • A StateFlow<UiState>: single source of truth for what the UI renders.
  • A sealed Event interface: user actions are sent via onEvent(...).
  • 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.

Dependency direction

Dependencies always point inward: featuredomaincore:network. The domain module has no Android dependency and runs on the JVM, which keeps business logic fast to unit test.


Module map

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

Key libraries and why they were chosen

UI — Jetpack Compose + Navigation 3

  • Compose (Material 3) for all UI. Screens are declarative and testable in isolation.
  • Navigation 3 (androidx.navigation3) manages a typed back stack of @Serializable NavKey destinations (Trending, Detail(movieId)). Navigation lives in :app (AppNavDisplay.kt); feature modules stay unaware of routing.
  • Coil 3 loads poster images via coil-compose with the OkHttp network fetcher, reusing the same HTTP stack philosophy as Ktor.

Network — Ktor + kotlinx.serialization

  • 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:network map DTOs to domain models; the domain layer never sees wire format types.

DI — Koin

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().

Coroutines + Flow

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.


Testing strategy

Unit tests — JUnit 5, MockK, Turbine, AssertJ

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:

  • CoroutineTest interface + TestCoroutineExtension — sets up UnconfinedTestDispatcher, replaces Dispatchers.Main, and injects a test DispatcherProvider.
  • Domain/DTO stubs (DomainStubs, DtoStubs) for consistent test fixtures.

Domain use cases, repository implementations, and ViewModels all have unit test coverage.

Screenshot tests — JUnit 5, TestParameterInjector, Paparazzi

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:verifyPaparazzi

UI / instrumented tests — Compose Test, Robot pattern, Orchestrator

Feature-level instrumented tests live in feature:trending/src/androidTest. They use:

  • Compose Test Rule (createComposeRule) to set screen content directly with stubbed UiState — no network, no ViewModel.
  • Robot patternTrendingRobot extends BaseRobot from :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" with clearPackageData and useTestStorageService for isolated, clean test runs.
./gradlew :feature:trending:connectedDebugAndroidTest

Note: 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.


Static analysis — Detekt

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-formatting plugin applied for ktlint-compatible formatting checks.
./gradlew detekt

Build and run

Requirements: 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 cAT

API key

TMDB 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.


Design decisions worth noting

  1. Feature modules own their DI modules (trendingModule, detailModule) but not navigation — keeps features reusable and navigation centralised in :app.
  2. 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.
  3. Screens accept state + callbacks: this was deliberate to maximise testability (Paparazzi, Compose Test, and @Preview all work without a ViewModel).
  4. 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.
  5. Sort/filter logic lives in domain use cases (FilterMoviesUseCase, SortMoviesUseCase, GetAvailableGenresUseCase) so it is unit-testable without Android or Compose.
  6. :test-data / :test-data-android split: JVM test utilities stay separate from Android Robot helpers, matching the same boundary as :core:common / :core:common-android.

Things left intentionally incomplete

  • Connection-specific error UI: TrendingViewModel has a commented TODO for distinguishing network errors from generic failures via isConnectionError().
  • 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 buildSrc or plugins (build-logic) will speed up the next feature development.

Project layout (quick reference)

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages