A Kotlin Multiplatform (KMP) product catalog app built with Compose Multiplatform, targeting both Android and iOS. It consumes the DummyJSON Products API to browse, search, filter, and cart products — all from a single shared codebase.
- Product catalog — browse products in a paginated 2-column grid showing name, price, thumbnail, and rating.
- Product details — tap a product to view title, description, brand, price, discount, rating, stock, dimensions, reviews, warranty, shipping info, and an image gallery.
- Search — keyword search integrated with the DummyJSON search API (
/products/search?q=...). Smart category matching redirects searches that match a known category name. - Category filtering — category chips fetched from the API and cached in Room; filtering calls the category-specific endpoint.
- Shopping cart — add/remove products, adjust quantity; cart state is persisted locally in Room.
- Adaptive layout — single-pane navigation on phones; list-detail split pane (40/60) on tablets (≥ 840 dp) with animated transitions.
- Offline support — product list is cached via Paging 3
RemoteMediator+ Room; product details are also cached for offline access. - Light & dark theme — follows the system theme with full Material 3 color schemes for both modes.
| iOS | Android |
|---|---|
ios-sample.mp4 |
android-sample.mp4 |
The project follows Clean Architecture with three clearly separated layers:
┌──────────────────────────────────────────────────────────┐
│ Presentation (UI + ViewModels) │
│ • Compose Multiplatform screens │
│ • StateFlow-based UI state management │
│ • Adaptive layouts (compact / expanded) │
├──────────────────────────────────────────────────────────┤
│ Domain (Use Cases + Models + Repository Interfaces) │
│ • GetProductsUseCase │
│ • SearchProductsUseCase │
│ • GetProductDetailUseCase │
│ • CartRepository interface │
│ • Domain models (ProductListItem, ProductDetail, etc.) │
├──────────────────────────────────────────────────────────┤
│ Data (Repository Impls + API + Database + Paging) │
│ • Ktor Client (API calls) │
│ • kotlinx.serialization (JSON parsing) │
│ • Room (local caching & cart persistence) │
│ • Paging 3 with RemoteMediator & PagingSource │
└──────────────────────────────────────────────────────────┘
| Concern | Technology |
|---|---|
| Language | Kotlin 2.3 (Multiplatform) |
| Shared UI | Compose Multiplatform 1.10 |
| Design System | Material 3 |
| Navigation | Navigation 3 (navigation3-ui alpha) |
| Networking | Ktor 3.4 (CIO + Android engine on Android, Darwin engine on iOS) |
| Serialization | kotlinx.serialization |
| Image Loading | Coil 3 (AsyncImage + Ktor network backend) |
| Local Database | Room 2.8 (KMP) + SQLite Bundled |
| Pagination | Paging 3 (RemoteMediator + PagingSource) |
| Dependency Injection | Koin 4.1 (koin-compose-viewmodel) |
| UI State | StateFlow in ViewModels |
| Build System | Gradle (Kotlin DSL) + AGP 9.0 + Version Catalog |
| KSP | Room compiler annotation processing |
ProductAlpha/
├── androidApp/ # Android application shell
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── kotlin/.../
│ ├── MainActivity.kt
│ └── ProductAlphaApplication.kt
├── composeApp/ # Shared KMP module (all business logic + UI)
│ ├── build.gradle.kts
│ ├── schemas/ # Room schema exports
│ └── src/
│ ├── commonMain/kotlin/com/productalpha/
│ │ ├── App.kt # Root composable (compact + expanded layouts)
│ │ ├── core/
│ │ │ ├── database/ # Room AppDatabase definition
│ │ │ │ ├── AppDatabase.kt
│ │ │ │ ├── AppDatabaseConstructor.kt
│ │ │ │ └── CreateDatabase.kt # expect fun (actual in androidMain/iosMain)
│ │ │ ├── navigation/ # Screen sealed class (Navigation 3 NavKey)
│ │ │ │ └── Navigation.kt # Screen.ProductList, Screen.ProductDetail, Screen.Cart
│ │ │ ├── network/ # ProductApi + HttpClient factory
│ │ │ │ └── ProductApi.kt
│ │ │ └── ui/ # Shared UI components
│ │ │ ├── ErrorScreen.kt
│ │ │ └── ErrorScreenPreviews.kt
│ │ ├── di/ # Core Koin modules
│ │ │ ├── KoinModule.kt # initKoin() entry point (assembles all modules)
│ │ │ ├── NetworkModule.kt # HttpClient, ProductApi
│ │ │ └── DatabaseModule.kt # AppDatabase
│ │ ├── features/
│ │ │ ├── productList/
│ │ │ │ ├── data/
│ │ │ │ │ ├── local/ # ProductEntity, ProductDao, CategoryEntity, CategoryDao, RemoteKeys, RemoteKeysDao
│ │ │ │ │ ├── mapper/ # ProductListMapper (ProductEntity↔Domain, ProductDto→Entity)
│ │ │ │ │ ├── model/ # ProductDto, ProductResponseDto (API DTOs)
│ │ │ │ │ ├── paging/ # ProductRemoteMediator, SearchProductsPagingSource
│ │ │ │ │ └── repository/ # ProductRepositoryImpl
│ │ │ │ ├── di/
│ │ │ │ │ └── ProductListModule.kt # productListModule (repo + use cases + VM)
│ │ │ │ ├── domain/
│ │ │ │ │ ├── model/ # ProductListItem, ProductListUiState, PaginatedProducts
│ │ │ │ │ ├── repository/ # ProductRepository interface
│ │ │ │ │ └── usecase/ # GetProductsUseCase, SearchProductsUseCase
│ │ │ │ └── presentation/
│ │ │ │ ├── ui/ # ProductListScreen, ProductListPreviews
│ │ │ │ └── viewmodel/ # ProductListViewModel
│ │ │ ├── productDetails/
│ │ │ │ ├── data/
│ │ │ │ │ ├── local/ # ProductDetailEntity, ProductDetailDao
│ │ │ │ │ ├── mapper/ # ProductDetailMapper (DTO↔Domain↔Entity)
│ │ │ │ │ ├── model/ # ProductDetailDto
│ │ │ │ │ └── repository/ # ProductDetailRepositoryImpl
│ │ │ │ ├── di/
│ │ │ │ │ └── ProductDetailModule.kt # productDetailModule (repo + use case + VM)
│ │ │ │ ├── domain/
│ │ │ │ │ ├── model/ # ProductDetail, ProductDetailUiState
│ │ │ │ │ ├── repository/ # ProductDetailRepository interface
│ │ │ │ │ └── usecase/ # GetProductDetailUseCase
│ │ │ │ └── presentation/
│ │ │ │ ├── ui/ # ProductDetailScreen, ProductDetailPreviews
│ │ │ │ └── viewmodel/ # ProductDetailViewModel
│ │ │ ├── cart/
│ │ │ │ ├── data/
│ │ │ │ │ ├── local/ # CartItemEntity, CartItemDao
│ │ │ │ │ ├── mapper/ # CartMapper (CartItemEntity→Domain)
│ │ │ │ │ └── repository/ # CartRepositoryImpl
│ │ │ │ ├── di/
│ │ │ │ │ └── CartModule.kt # cartModule (repo + use cases + VM)
│ │ │ │ ├── domain/
│ │ │ │ │ ├── model/ # CartItem
│ │ │ │ │ ├── repository/ # CartRepository interface
│ │ │ │ │ └── usecase/ # GetCartItemsUseCase, ManageCartUseCase
│ │ │ │ └── presentation/
│ │ │ │ ├── ui/ # CartScreen, CartPreviews
│ │ │ │ └── viewmodel/ # CartViewModel
│ │ ├── theme/ # Light & dark Material 3 color schemes (Colors.kt, Theme.kt)
│ │ └── utils/ # FormatUtils (KMP-safe price formatting)
│ ├── androidMain/ # Android-specific expect/actual implementations
│ │ └── kotlin/.../
│ │ ├── core/database/CreateDatabase.android.kt # Room DB builder
│ │ └── di/KoinModule.android.kt # platformModule (actual)
│ ├── iosMain/ # iOS-specific expect/actual implementations
│ │ └── kotlin/.../
│ │ ├── MainViewController.kt # ComposeApp → UIViewController bridge
│ │ ├── core/database/CreateDatabase.ios.kt # Room DB builder
│ │ └── di/KoinModule.ios.kt # platformModule (actual)
│ └── commonTest/ # Unit tests (use case tests with mock repositories)
│ └── kotlin/.../
│ ├── GetCartItemsUseCaseTest.kt # Tests for cart functionality
│ ├── GetProductsUseCaseTest.kt # Tests for product listing and categories
│ ├── GetProductDetailUseCaseTest.kt # Tests for product details
│ ├── SearchProductsUseCaseTest.kt # Tests for search functionality
│ └── MockRepository.kt # Shared mock implementations for tests
├── iosApp/ # iOS application shell (SwiftUI + ComposeView)
│ ├── iosApp.xcodeproj/
│ └── iosApp/
│ ├── iOSApp.swift
│ ├── ContentView.swift # Bridges ComposeApp via UIViewControllerRepresentable
│ └── Assets.xcassets/
└── gradle/
├── libs.versions.toml # Version catalog
└── wrapper/
All data is fetched from DummyJSON:
| Endpoint | Purpose |
|---|---|
GET /products?limit=&skip= |
Paginated product list |
GET /products/search?q=&limit=&skip= |
Keyword search |
GET /products/{id} |
Single product detail |
GET /products/category/{name}?limit=&skip= |
Products by category |
GET /products/category-list |
All category names |
- Android Studio Ladybug or newer (with Kotlin Multiplatform plugin)
- Xcode 15+ (for iOS)
- JDK 11+
# Build debug APK
./gradlew :androidApp:assembleDebug
# Install on connected device / emulator
./gradlew :androidApp:installDebugOr use the androidApp run configuration in Android Studio.
# Compile the shared framework for iOS Simulator
./gradlew :composeApp:compileKotlinIosSimulatorArm64
# Then open the Xcode project and run
open iosApp/iosApp.xcodeprojBuild & run the iosApp target from Xcode on a simulator or device.
./gradlew :composeApp:allTestsThe project uses Koin with modular configuration:
| Module | Provides |
|---|---|
platformModule |
Platform-specific RoomDatabase.Builder (expect/actual per platform) |
networkModule |
HttpClient, ProductApi |
databaseModule |
AppDatabase |
productListModule |
ProductDao, RemoteKeysDao, CategoryDao, ProductRepository, GetProductsUseCase, SearchProductsUseCase, ProductListViewModel |
productDetailModule |
ProductDetailDao, ProductDetailRepository, GetProductDetailUseCase, ProductDetailViewModel |
cartModule |
CartItemDao, CartRepository, GetCartItemsUseCase, ManageCartUseCase, CartViewModel |
Each feature owns its own Koin module (repository + use cases + ViewModel), keeping DI configuration co-located with the feature code.
Initialization: initKoin() is called from ProductAlphaApplication on Android and MainViewController on iOS.
Unit tests live in composeApp/src/commonTest/ and cover every feature's use cases:
Product List (MockProductRepository)
GetProductsUseCaseTest(6 tests) — verifies delegation togetProducts()vsgetProductsByCategory()based on category filter; validates category list retrieval; confirmsrefreshCategories()delegation.SearchProductsUseCaseTest(2 tests) — verifies search query delegation, including empty query edge case.
Product Details (MockProductDetailRepository)
GetProductDetailUseCaseTest(4 tests) — verifies correct product ID delegation; validates all returned fields including price, rating, reviews, dimensions, and images.
Cart (MockCartRepository)
GetCartItemsUseCaseTest(2 tests) — verifies empty initial state and reactivity when items are added.ManageCartUseCaseTest(7 tests) — verifies add, remove, increment, decrement, clear, duplicate-add quantity merging, and auto-removal when quantity reaches zero.
All tests use mock repository implementations that track method calls and operate on in-memory data.
- Navigation 3 alpha — The app uses Jetpack Navigation 3 (
navigation3-uialpha) with serializableNavKeysealed classes. This is a bleeding-edge API and may change.
| Property | Value |
|---|---|
| Kotlin | 2.3.10 |
| AGP | 9.0.1 |
| Compose Multiplatform | 1.10.2 |
compileSdk |
36 |
minSdk |
27 |
targetSdk |
36 |
| JVM Target | 11 |
| Room DB Version | 3 |
| Gradle Configuration Cache | ✅ |