Skip to content

UsmanAnsari/StockWise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

17 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“¦ StockWise

StockWise Banner

Kotlin Android Jetpack Compose Tests

Architecture Pattern Testing

Modern Android Inventory Management App

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.


πŸ“± Features

Core Functionality

  • πŸ“Š 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

Technical Highlights

  • πŸ—οΈ 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

User Experience

  • πŸ” 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

πŸ“Έ Screenshots

Dashboard Dashboard Product Product Filter/Sort Product Detail
Dashboard Dashboard 2 Products Products Filer/Sort Detail
Suppliers Categories Stock Movement Stock Movement Stock Movement
Suppliers Categories Stock Stock Stock

Dashboard Products Stock Movements
Dashboard Products Stock Movements


πŸ› οΈ Tech Stack

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

πŸ—οΈ Architecture

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.

Clean Architecture Layers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    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)     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

MVI Pattern

The app follows unidirectional data flow for predictable state management:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         MVI FLOW                           β”‚
β”‚                                                            β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚     VIEW     │──────────────┐         β”‚
β”‚     β”‚              β”‚   (Screen)   β”‚              β”‚         β”‚
β”‚     β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚         β”‚
β”‚     β”‚                                            β”‚         β”‚
β”‚   STATE                                        EVENT       β”‚
β”‚ (Immutable)                                  (Intent)      β”‚
β”‚     β”‚                                            β”‚         β”‚
β”‚     β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚         β”‚
β”‚     └──────────────│  VIEWMODEL   β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚                           β”‚                                β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚                    β”‚   USE CASE   β”‚                        β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚                           β”‚                                β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚                    β”‚  REPOSITORY  β”‚                        β”‚
β”‚                    β”‚  (Room DAOs) β”‚                        β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ—„οΈ Database Schema

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.

Entity Relationship Diagram

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
Loading

Junction Table: Sale Items (M:N)

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.

Database Design Highlights

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

πŸ“Š Use Cases Overview

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 count
  • GetDailySalesSummaryUseCase β€” Daily sales metrics
  • GetLowStockProductsUseCase β€” Products below reorder threshold
  • GetRecentSalesUseCase β€” Recent sales activity feed
Product Use Cases (8)
  • GetProductsWithDetailsUseCase β€” Products with joined category/supplier data
  • GetProductWithDetailsUseCase β€” Single product with full details
  • GetProductByIdUseCase β€” Lightweight ID lookup
  • FilterProductsUseCase β€” Filter and sort by multiple criteria
  • ValidateProductUseCase β€” Form validation including SKU uniqueness
  • CreateProductUseCase β€” Create new product
  • UpdateProductUseCase β€” Update existing product
  • DeleteProductUseCase β€” Soft delete (preserves historical records)
Category Use Cases (7)
  • GetCategoriesUseCase β€” All categories
  • GetCategoriesWithProductCountUseCase β€” Categories with live product counts
  • GetCategoryByIdUseCase β€” Single category lookup
  • ValidateCategoryUseCase β€” Name uniqueness validation
  • CreateCategoryUseCase β€” Create category
  • UpdateCategoryUseCase β€” Update category
  • DeleteCategoryUseCase β€” Delete with product-count protection
Supplier Use Cases (7)
  • GetSuppliersUseCase β€” All suppliers
  • GetSuppliersWithProductCountUseCase β€” Suppliers with live product counts
  • GetSupplierByIdUseCase β€” Single supplier lookup
  • ValidateSupplierUseCase β€” Contact details validation
  • CreateSupplierUseCase β€” Create supplier
  • UpdateSupplierUseCase β€” Update supplier
  • DeleteSupplierUseCase β€” Delete with product-count protection
Stock Use Cases (6)
  • GetStockMovementsForProductUseCase β€” Full movement history for a product
  • GetStockMovementsSummaryUseCase β€” Totals by movement type
  • FilterStockMovementsUseCase β€” Filter by type and date range
  • AddStockUseCase β€” Inbound stock movement
  • RemoveStockUseCase β€” Outbound stock movement
  • AdjustStockUseCase β€” Manual stock level correction

🎯 Key Implementation Highlights

1. MVI Contract Pattern

// 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
}

2. Delete Protection Pattern

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)
    }
}

3. Use Case Validation with SKU Uniqueness

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)
    }
}

πŸ§ͺ Testing

StockWise has 95+ tests across unit and instrumented test suites, covering business logic, state management, and database operations.

Test Coverage Breakdown

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

Example: ViewModel Test with Turbine

@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()
}

Example: DAO Test with In-Memory Room

@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)
}

🚧 Scope: Portfolio vs Production

What's Implemented

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

Production Enhancements

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

πŸŽ“ What I Learned

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.


πŸš€ Getting Started

Prerequisites

  • Android Studio Hedgehog (2023.1.1) or newer
  • JDK 17
  • Android SDK 29+
  • No API keys required β€” fully local/offline app

Installation

  1. Clone the repository
git clone https://github.com/UsmanAnsari/StockWise.git
cd StockWise
  1. Build and Run
./gradlew installDebug
# Or click Run ▢️ in Android Studio

Running Tests

# Unit tests
./gradlew test

# Instrumented tests (requires connected device or emulator)
./gradlew connectedAndroidTest

πŸ“ Project Structure

app/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

πŸ—ΊοΈ Roadmap

  • 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

πŸ‘€ Author

Usman Ali Ansari


Built with ❀️ to demonstrate production-ready Android development