A book-discovery app: search the OpenLibrary catalog, open a book to read its details, and save books as favorites that persist across sessions.
Built with Kotlin Multiplatform + Compose Multiplatform. Only the Android target is wired up for this delivery; the architecture lives in commonMain so iOS/web can be added later as an additive step.
Status: in active development. Architecture, navigation, dependency injection, and CI are in place; the data layer and feature screens are being built issue-by-issue — see docs/BACKLOG.md.
| Concern | Choice |
|---|---|
| Language | Kotlin |
| UI | Compose Multiplatform + Material 3 (incl. Material 3 Adaptive) |
| Architecture | Clean Architecture + MVVM |
| Navigation | JetBrains Navigation3 with adaptive list/detail panes |
| DI | Koin |
| Networking | Ktor Client + kotlinx.serialization |
| Persistence | Room (KMP) for favorites |
| Images | Coil 3 |
| Async/state | Coroutines, Flow / StateFlow |
| Testing | kotlin-test, kotlinx-coroutines-test, Turbine, Mokkery, Kotest |
| Quality | detekt, ktlint, Kover (coverage), lefthook |
Clean Architecture with three layers, all in the shared KMP module's commonMain (so they are platform-agnostic). The only platform seam is Koin's platformModule (expect/actual).
Composable → ViewModel → UseCase → Repository → RemoteSource (Ktor) / LocalSource (Room)
shared/commonMain/com.darioossa.openbooks/
├── data/ BooksRepository · remote/ (Ktor) · local/ (Room)
├── domain/ entities/Book · dataSource/BooksDataSource · use cases
├── presentation/ bookList/ · bookDetail/ · favorites/ (MVVM ViewModels + screens)
├── navigation/ Route · NavigationRoot · NavigationEntries
└── di/ KoinInit · Modules
- Presentation (MVVM): ViewModels expose UI state as
StateFlow; Compose collects withcollectAsStateWithLifecycle(). ViewModels never hold AndroidContextor Compose types, and survive configuration changes. UI state is modelled as sealed classes (loading / success / empty / error). - Domain: pure Kotlin use cases over a
BooksDataSourceinterface — no framework dependencies. - Data:
BooksRepositoryimplements the domain interface and combines a remote source (Ktor → OpenLibrary) with a local source (Room → favorites). - Navigation: Navigation3 + Material 3 Adaptive renders list and detail side-by-side on large screens automatically.
The domain language (notably Book = an OpenLibrary Work, identified by a Work key) is documented in CONTEXT.md.
Detailed rationale lives in docs/adr/:
- Compose Multiplatform, Android-only target for now — earns the CMP bonus and keeps the codebase multiplatform-ready, without spending the budget on iOS/web app shells. (ADR-0002)
- Ktor instead of Retrofit — the challenge permits justified library substitutions; Retrofit is JVM/Android-only, so Ktor keeps networking in
commonMainand the data layer multiplatform. (ADR-0001) - OpenLibrary Search API as the single list source, with an empty initial screen — one endpoint serves list + search + pagination; the search-driven flow exercises all three required UI states (empty / loading / error). Detail is fetched per-Work. (ADR-0003)
- Manual infinite scroll (via
derivedStateOf), not Paging 3 — the mandatory requirement is "pagination or infinite scroll"; manual paging avoids a heavier integration. Paging 3 is a possible future bonus. - Koin over Hilt — KMP-compatible DI. Mokkery over Mockito — KMP-compatible mocking.
Prerequisites: JDK 17, Android SDK (compileSdk 37), Android Studio (or the Gradle CLI). minSdk is 24.
# Build the Android app
./gradlew :androidApp:assembleDebug
# Install on a connected device/emulator
./gradlew :androidApp:installDebug
# Run shared tests
./gradlew :shared:allTests
# Run static analysis / formatting checks
./gradlew detektAll ktlintAllYou can also use the run configurations in Android Studio's toolbar.
Logic layers (ViewModels, repository) are built test-first (TDD). Tests run in commonTest (fast, no Android instrumentation) using runTest, Turbine for Flow/StateFlow assertions, and Mokkery for mocking. BaseViewModelTest sets Dispatchers.Main to a test dispatcher. Compose UI tests (createComposeRule) are a bonus, not part of the core suite.
GitHub Actions, kept lean for fast PR feedback. main is the protected, always-green trunk; every change lands via a feature branch and a PR that must pass pr-checks.
pr-checks(PR gate) —actionlint, Gradle wrapper validation, shared quality gates (detektAll ktlintAll :shared:allTests), and Android target verification (:androidApp:lintDebug :androidApp:assembleDebug). Only the Android target is verified today; the matrix is structured to add iOS later.ci-main— generates Kover coverage reports on the main branches.security— Gitleaks secret scanning and dependency review on PRs.- Dependabot — weekly Gradle and GitHub Actions dependency-update PRs.
Local lefthook hooks (pre-commit formatting, pre-push heavier checks) bring part of this feedback closer to the developer machine; GitHub Actions remains the source of truth.
Work is tracked as issue-style items in docs/BACKLOG.md, each delivered as a single PR. Current trade-offs and pending work:
- Search-only home screen — no seeded/trending list on launch; the screen is empty until the user searches (deliberate, see ADR-0003).
- Android-only — iOS/web targets are not wired up; the structure makes them an additive step, not a rewrite.
- No Paging 3 yet — manual infinite scroll is used; Paging 3 is a possible bonus.
- Pending: remote source (Ktor + Search API), favorites persistence (Room), the list/detail/favorites screens and their ViewModels, and rounding the suite out to the required 3–5 meaningful tests.