A production-grade inventory management application demonstrating Clean Architecture, MVI pattern, complex relational database design (including M:N relationships), and comprehensive test coverage.
Built as a portfolio project to showcase local-first architecture and domain complexity for mid-level to senior Android engineering roles.
- π Dashboard - Real-time inventory overview with key metrics, low stock indicator, and recent activity
- π¦ Product Management - Full CRUD operations with search, filtering, and sorting capabilities
- π Categories - Organize products with color-coded categories and delete protection
- π Suppliers - Manage supplier information with contact details
- π Stock Tracking - Record stock movements (in/out/adjustments) with complete history
- ποΈ Clean Architecture β Strict layer separation with dependency inversion across all 5 feature modules
- π MVI Pattern β Unidirectional data flow with immutable state across all screens
- ποΈ Complex Database Schema β 6 Room entities including a M:N junction table with snapshot data
- π Referential Integrity β Soft delete and delete-protection patterns to preserve data consistency
- π 32 Use Cases β Every operation isolated to a single-responsibility use case
- π§ͺ 95+ Tests β Unit tests for use cases and ViewModels, instrumented DAO tests
- π Advanced Search - Search products by name or SKU with real-time results
- ποΈ Smart Filtering - Filter by category, stock status (in stock, low stock, out of stock)
- π Sorting Options - Sort by name, price, stock level, or last updated
| Dashboard | Dashboard | Product | Product Filter/Sort | Product Detail |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
| Suppliers | Categories | Stock Movement | Stock Movement | Stock Movement |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
| Dashboard | Products | Stock Movements |
|---|---|---|
![]() |
![]() |
![]() |
| Category | Technology | Why This Choice |
|---|---|---|
| Language | Kotlin 2.2.21 | Coroutines, Flow, null safety, sealed classes for MVI |
| UI Framework | Jetpack Compose + Material 3 | Declarative UI eliminates view binding boilerplate |
| Architecture | Clean Architecture + MVI | Enforces testability and unidirectional data flow |
| Dependency Injection | Hilt | Compile-time DI with less boilerplate than manual Dagger |
| Database | Room (SQLite) | Type-safe SQL with native Flow support for reactive UI |
| Async | Kotlin Coroutines + Flow | Native async/reactive β no RxJava overhead |
| Navigation | Jetpack Navigation Compose | Type-safe nav graph integrated with Compose |
| Testing | JUnit, MockK, Turbine, Truth | Kotlin-first tools; Turbine simplifies Flow assertions |
StockWise follows Clean Architecture with strict layer boundaries and dependency inversion. The domain layer has zero Android dependencies β all business logic is pure Kotlin, making it fully testable without an emulator.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PRESENTATION LAYER β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β Screens β β ViewModels β β Contracts (MVI) β β
β β (Compose) β β (State) β β State/Event/Effect β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DOMAIN LAYER β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β Use Cases β β Models β βRepository Interfacesβ β
β β (32 total) β β (Domain) β β β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β DATA LAYER β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β βRepositories β β DAOs β β Entities β β
β β (Impl) β β (Room) β β (6 entities) β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The app follows unidirectional data flow for predictable state management:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MVI FLOW β
β β
β ββββββββββββββββ β
β βββββββββββββββΊβ VIEW ββββββββββββββββ β
β β β (Screen) β β β
β β ββββββββββββββββ β β
β β β β
β STATE EVENT β
β (Immutable) (Intent) β
β β β β
β β ββββββββββββββββ β β
β ββββββββββββββββ VIEWMODEL ββββββββββββββββ β
β ββββββββ¬ββββββββ β
β β β
β ββββββββΌββββββββ β
β β USE CASE β β
β ββββββββ¬ββββββββ β
β β β
β ββββββββΌββββββββ β
β β REPOSITORY β β
β β (Room DAOs) β β
β ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
StockWise uses Room with 6 entities including a junction table for a many-to-many relationship between Products and Sales β a pattern commonly required in real-world inventory and e-commerce systems.
erDiagram
CATEGORIES ||--o{ PRODUCTS : "1:N has"
SUPPLIERS ||--o{ PRODUCTS : "1:N supplies"
PRODUCTS ||--o{ STOCK_MOVEMENTS : "1:N tracks"
PRODUCTS }o--o{ SALES : "M:N via SaleItems"
SALES ||--o{ SALE_ITEMS : "1:N contains"
PRODUCTS ||--o{ SALE_ITEMS : "1:N sold as"
CATEGORIES
SUPPLIERS
PRODUCTS
STOCK_MOVEMENTS
SALE_ITEMS
SALES
The SALE_ITEMS table resolves the many-to-many relationship between Products and Sales while also preserving a data snapshot at the time of sale β a critical pattern for financial accuracy in inventory systems:
ββββββββββββ βββββββββββββββ ββββββββββββ
β PRODUCTS βββββββββ SALE_ITEMS ββββββββΊβ SALES β
β β 1:N β (Junction) β N:1 β β
β id β β productId β β id β
β β β saleId β β β
ββββββββββββ β quantity β ββββββββββββ
β unitPrice* β
β productName*β
βββββββββββββββ
* Snapshot fields β preserve data at time of sale,
so historical records remain accurate even if
the product is later updated or deleted.
| Feature | Implementation | Purpose |
|---|---|---|
| M:N Relationship | SaleItems junction table |
Links Products β Sales without data duplication |
| Data Snapshots | Price + name stored in SaleItems |
Historical accuracy for past sales records |
| Soft Delete | isActive flag on Products |
Preserves referential integrity across foreign keys |
| Audit Trail | StockMovements table |
Every inventory change is permanently recorded |
| Delete Protection | Count-check before delete | Categories/Suppliers can't be removed while linked to products |
StockWise implements 32 Use Cases following the Single Responsibility Principle β every operation is isolated, independently testable, and maps to exactly one business action.
Dashboard Use Cases (4)
GetInventoryStatsUseCaseβ Total products, stock value, categories countGetDailySalesSummaryUseCaseβ Daily sales metricsGetLowStockProductsUseCaseβ Products below reorder thresholdGetRecentSalesUseCaseβ Recent sales activity feed
Product Use Cases (8)
GetProductsWithDetailsUseCaseβ Products with joined category/supplier dataGetProductWithDetailsUseCaseβ Single product with full detailsGetProductByIdUseCaseβ Lightweight ID lookupFilterProductsUseCaseβ Filter and sort by multiple criteriaValidateProductUseCaseβ Form validation including SKU uniquenessCreateProductUseCaseβ Create new productUpdateProductUseCaseβ Update existing productDeleteProductUseCaseβ Soft delete (preserves historical records)
Category Use Cases (7)
GetCategoriesUseCaseβ All categoriesGetCategoriesWithProductCountUseCaseβ Categories with live product countsGetCategoryByIdUseCaseβ Single category lookupValidateCategoryUseCaseβ Name uniqueness validationCreateCategoryUseCaseβ Create categoryUpdateCategoryUseCaseβ Update categoryDeleteCategoryUseCaseβ Delete with product-count protection
Supplier Use Cases (7)
GetSuppliersUseCaseβ All suppliersGetSuppliersWithProductCountUseCaseβ Suppliers with live product countsGetSupplierByIdUseCaseβ Single supplier lookupValidateSupplierUseCaseβ Contact details validationCreateSupplierUseCaseβ Create supplierUpdateSupplierUseCaseβ Update supplierDeleteSupplierUseCaseβ Delete with product-count protection
Stock Use Cases (6)
GetStockMovementsForProductUseCaseβ Full movement history for a productGetStockMovementsSummaryUseCaseβ Totals by movement typeFilterStockMovementsUseCaseβ Filter by type and date rangeAddStockUseCaseβ Inbound stock movementRemoveStockUseCaseβ Outbound stock movementAdjustStockUseCaseβ Manual stock level correction
// ProductsContract.kt β Clean separation of UI concerns
data class ProductsState(
val isLoading: Boolean = true,
val products: List<ProductWithDetails> = emptyList(),
val searchQuery: String = "",
val selectedFilter: StockFilter = StockFilter.ALL,
val sortOption: SortOption = SortOption.NAME
) : UiState {
// Derived state β computed once, not duplicated
val isEmpty: Boolean get() = products.isEmpty() && !isLoading
}
sealed interface ProductsEvent : UiEvent {
data object LoadProducts : ProductsEvent
data class OnSearchQueryChanged(val query: String) : ProductsEvent
data class OnFilterChanged(val filter: StockFilter) : ProductsEvent
data class OnSortChanged(val sort: SortOption) : ProductsEvent
data class OnDeleteProduct(val productId: Long) : ProductsEvent
}
sealed interface ProductsEffect : UiEffect {
data class NavigateToDetail(val productId: Long) : ProductsEffect
data class ShowSnackbar(val message: String) : ProductsEffect
}class DeleteCategoryUseCase @Inject constructor(
private val categoryRepository: CategoryRepository,
private val productRepository: ProductRepository
) {
suspend operator fun invoke(categoryId: Long): Result<Unit> {
// Guard: check for linked products before deletion
val productCount = productRepository.getProductCountByCategory(categoryId)
if (productCount > 0) {
return Result.failure(
CategoryHasProductsException(
"Cannot delete: $productCount products are linked to this category"
)
)
}
return categoryRepository.deleteCategory(categoryId)
}
}class ValidateProductUseCase @Inject constructor(
private val productRepository: ProductRepository
) {
suspend operator fun invoke(params: ValidationParams): ValidationResult {
val errors = mutableMapOf<String, String>()
if (params.name.isBlank()) {
errors[FIELD_NAME] = "Product name is required"
}
// SKU uniqueness β exclude current product on edit
val existingProduct = productRepository.getProductBySku(params.sku)
if (existingProduct != null && existingProduct.id != params.excludeProductId) {
errors[FIELD_SKU] = "SKU already exists"
}
return ValidationResult(isValid = errors.isEmpty(), errors = errors)
}
}StockWise has 95+ tests across unit and instrumented test suites, covering business logic, state management, and database operations.
| Layer | Type | What's Tested |
|---|---|---|
| Use Cases | Unit (JUnit + MockK) | Business logic, validation, delete protection |
| ViewModels | Unit (Turbine) | State transitions, event handling, effects |
| DAOs | Instrumented (Room in-memory) | Queries, relationships, cascade behaviour |
@Test
fun `search query filters products correctly`() = runTest {
viewModel = createViewModel()
advanceUntilIdle()
viewModel.onEvent(ProductsEvent.OnSearchQueryChanged("iPhone"))
advanceUntilIdle()
val state = viewModel.uiState.value
assertThat(state.products.all {
it.product.name.contains("iPhone", ignoreCase = true)
}).isTrue()
}@Test
fun `getProductCountByCategory returns correct count`() = runTest {
// Arrange β insert category and linked products
val categoryId = categoryDao.insert(testCategory)
productDao.insert(testProduct1.copy(categoryId = categoryId))
productDao.insert(testProduct2.copy(categoryId = categoryId))
// Assert
val count = productDao.getProductCountByCategory(categoryId)
assertThat(count).isEqualTo(2)
}| Feature | Status | Notes |
|---|---|---|
| Product CRUD | β Complete | With validation and soft delete |
| Inventory Tracking | β Complete | Full audit trail via StockMovements |
| M:N Database Schema | β Complete | Junction table with data snapshots |
| Delete Protection | β Complete | Referential integrity enforced in use cases |
| 32 Use Cases | β Complete | Every operation single-responsibility |
| 95+ Tests | β Complete | Unit + instrumented coverage |
| CI/CD Pipeline | π Planned | GitHub Actions β next phase |
In a production app, I would additionally implement:
| Enhancement | Why | Complexity |
|---|---|---|
| CI/CD Pipeline | Automated testing on every push | Low (GitHub Actions) |
| Barcode Scanning | Faster stock intake for physical warehouses | Medium (CameraX + ML Kit) |
| Reports & Analytics | Charts for stock trends and sales performance | Medium (MPAndroidChart) |
| Cloud Sync | Multi-device access for business teams | High (backend + auth) |
| Export (CSV/PDF) | Reporting for accounting integrations | Medium |
Database Design
M:N relationships require careful thought β A product can appear in many sales, and a sale contains many products. The naive approach (storing a list in one table) doesn't work in relational databases. The junction table pattern solves this cleanly while also enabling snapshot data.
Snapshot data is a production requirement β If you store only a foreign key to the product in SaleItems, and the product price changes later, all historical sale records become inaccurate. Storing unitPrice and productName at the time of sale preserves financial history correctly.
Soft delete protects your data β Hard-deleting a product that appears in historical stock movements or sales would corrupt your audit trail. The isActive flag lets you "remove" it from the UI while keeping the data intact.
Architecture at Scale
32 use cases sounds like a lot β it isn't β Each use case is 10β30 lines of pure Kotlin. The discipline of one-operation-per-class means every piece of business logic is independently testable and easy to locate. When a bug appears in delete protection, you go to exactly one file.
Delete protection belongs in the domain layer β My first instinct was to handle this in the ViewModel. Moving it to a use case means it applies regardless of which screen triggers the delete, and it's testable without any Android dependencies.
Computed state beats duplicated state β The isEmpty computed property on ProductsState avoids the bug where products.isEmpty() and a separate isEmpty flag get out of sync.
Testing Strategy
In-memory Room databases are fast and reliable β Using Room.inMemoryDatabaseBuilder() in instrumented tests gives you a real database without touching disk. DAO tests run quickly and catch query issues that unit tests can't find.
MockK's coEvery is essential for suspend functions β Testing use cases that call suspend repository methods requires coroutine-aware mocking. MockK handles this elegantly.
Test the unhappy path β My most valuable tests are the ones that verify delete protection throws the right exception. The happy path rarely reveals architecture problems.
- Android Studio Hedgehog (2023.1.1) or newer
- JDK 17
- Android SDK 29+
- No API keys required β fully local/offline app
- Clone the repository
git clone https://github.com/UsmanAnsari/StockWise.git
cd StockWise- Build and Run
./gradlew installDebug
# Or click Run βΆοΈ in Android Studio# Unit tests
./gradlew test
# Instrumented tests (requires connected device or emulator)
./gradlew connectedAndroidTestapp/src/main/java/com/uansari/stockwise/
β
βββ π data/ # Data Layer
β βββ local/
β β βββ dao/ # Room DAOs (Products, Categories, Suppliers, Stock, Sales)
β β βββ entity/ # Room Entities & Relation classes
β β βββ StockWiseDatabase.kt # Room Database & TypeConverters
β βββ repository/ # Repository implementations
β
βββ π domain/ # Domain Layer (zero Android dependencies)
β βββ model/ # Domain models
β βββ repository/ # Repository interfaces
β βββ usecase/ # 32 Use Cases
β βββ dashboard/
β βββ product/
β βββ category/
β βββ supplier/
β βββ stock/
β
βββ π ui/ # Presentation Layer
β βββ base/ # Base MVI classes (UiState, UiEvent, UiEffect)
β βββ components/ # Shared Compose components
β βββ navigation/ # Nav graph & bottom navigation
β βββ dashboard/
β βββ products/
β βββ categories/
β βββ suppliers/
β βββ stock/
β
βββ π di/ # Dependency Injection (Hilt modules)
β βββ DatabaseModule.kt
β βββ RepositoryModule.kt
β
βββ π util/ # Shared utilities and extensions
- Phase 1: Database design & Room foundation
- Phase 2: Clean Architecture + MVI implementation
- Phase 3: 95+ tests (unit + instrumented)
- Phase 4: CI/CD Pipeline (GitHub Actions)
- Phase 5: Sales/POS module
- Phase 6: Reports & Analytics
- Phase 7: Barcode scanning
Usman Ali Ansari
- πΌ LinkedIn: usman1ansari
- π GitHub: @UsmanAnsari
- π§ Email: usman10ansari@gmail.com
Built with β€οΈ to demonstrate production-ready Android development













