Skip to content

Repository files navigation

ProductAlpha — Product Browser (KMP)

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.


Features

  1. Product catalog — browse products in a paginated 2-column grid showing name, price, thumbnail, and rating.
  2. Product details — tap a product to view title, description, brand, price, discount, rating, stock, dimensions, reviews, warranty, shipping info, and an image gallery.
  3. Search — keyword search integrated with the DummyJSON search API (/products/search?q=...). Smart category matching redirects searches that match a known category name.
  4. Category filtering — category chips fetched from the API and cached in Room; filtering calls the category-specific endpoint.
  5. Shopping cart — add/remove products, adjust quantity; cart state is persisted locally in Room.
  6. Adaptive layout — single-pane navigation on phones; list-detail split pane (40/60) on tablets (≥ 840 dp) with animated transitions.
  7. Offline support — product list is cached via Paging 3 RemoteMediator + Room; product details are also cached for offline access.
  8. Light & dark theme — follows the system theme with full Material 3 color schemes for both modes.

Demo

iOS Android
ios-sample.mp4
android-sample.mp4

Architecture Overview

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          │
└──────────────────────────────────────────────────────────┘

Tech Stack

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

Project Structure

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/

API Endpoints

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

How to Build and Run

Prerequisites

  • Android Studio Ladybug or newer (with Kotlin Multiplatform plugin)
  • Xcode 15+ (for iOS)
  • JDK 11+

Android

# Build debug APK
./gradlew :androidApp:assembleDebug

# Install on connected device / emulator
./gradlew :androidApp:installDebug

Or use the androidApp run configuration in Android Studio.

iOS

# Compile the shared framework for iOS Simulator
./gradlew :composeApp:compileKotlinIosSimulatorArm64

# Then open the Xcode project and run
open iosApp/iosApp.xcodeproj

Build & run the iosApp target from Xcode on a simulator or device.

Run Tests

./gradlew :composeApp:allTests

Dependency Injection

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


Testing

Unit tests live in composeApp/src/commonTest/ and cover every feature's use cases:

Product List (MockProductRepository)

  • GetProductsUseCaseTest (6 tests) — verifies delegation to getProducts() vs getProductsByCategory() based on category filter; validates category list retrieval; confirms refreshCategories() 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.


Trade-offs and Assumptions

  • Navigation 3 alpha — The app uses Jetpack Navigation 3 (navigation3-ui alpha) with serializable NavKey sealed classes. This is a bleeding-edge API and may change.

Build Configuration

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages