Skip to content

🍰 Production-ready Android app demonstrating Clean Architecture with multi-module setup. Built with Jetpack Compose, Material 3, MVI pattern, Hilt DI, Retrofit, Room, and Kotlin Coroutines. Perfect for learning enterprise Android development best practices.

License

Notifications You must be signed in to change notification settings

eslamfaisal/android-compose-clean-architecture-sample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🍰 BakingApp

Enterprise Android Clean Architecture Sample

Kotlin Compose Hilt Architecture License


A production-ready, scalable Android application showcasing modern development practices, clean architecture, and best-in-class libraries.

Features β€’ Architecture β€’ Tech Stack β€’ Getting Started β€’ Documentation


✨ Highlights

πŸ—οΈ Clean Architecture πŸ“± Modern UI πŸ§ͺ Testable πŸ”’ Secure
Multi-module setup with clear separation of concerns Jetpack Compose with Material 3 Design Comprehensive unit & UI tests NDK/C++ API keys + Encrypted storage

πŸ“Έ Screenshots

Login Home Recipe Details
πŸ” Secure Authentication 🏠 Recipe Discovery πŸ“– Step-by-Step Guide

🎯 Features

πŸ” Authentication

  • Email/password validation with real-time feedback
  • Secure token storage using EncryptedSharedPreferences
  • Loading states with smooth animations
  • Comprehensive error handling

🏠 Recipe Discovery

  • Beautiful recipe cards with images
  • Pull-to-refresh functionality
  • Category-based filtering
  • Debounced search for performance
  • Favorite toggle with local persistence

πŸ“– Recipe Details

  • Complete ingredients list with quantities
  • Step-by-step cooking instructions
  • Difficulty badges (Easy, Medium, Hard)
  • Prep/cook time information
  • Video support for cooking steps

πŸ—οΈ Architecture

BakingApp follows Clean Architecture principles combined with MVI (Model-View-Intent) pattern, ensuring:

  • βœ… Separation of Concerns - Each layer has a single responsibility
  • βœ… Testability - Business logic is isolated and easily testable
  • βœ… Scalability - New features can be added without affecting existing code
  • βœ… Maintainability - Clear boundaries make the codebase easy to understand

Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           PRESENTATION LAYER                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚    Screens     β”‚    β”‚   ViewModels   β”‚    β”‚       UI State          β”‚   β”‚
β”‚  β”‚   (Compose)    │◄───│     (MVI)      │───►│    (Immutable)          β”‚   β”‚
β”‚  β”‚                β”‚    β”‚                β”‚    β”‚                         β”‚   β”‚
β”‚  β”‚  β€’ LoginScreen β”‚    β”‚ β€’ LoginVM      β”‚    β”‚ β€’ LoginUiState          β”‚   β”‚
β”‚  β”‚  β€’ HomeScreen  β”‚    β”‚ β€’ HomeVM       β”‚    β”‚ β€’ HomeUiState           β”‚   β”‚
β”‚  β”‚  β€’ DetailScreenβ”‚    β”‚ β€’ DetailVM     β”‚    β”‚ β€’ RecipeDetailUiState   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                β”‚                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚ invoke()
                                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                             DOMAIN LAYER                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Use Cases    β”‚    β”‚    Entities    β”‚    β”‚  Repository Interfaces  β”‚   β”‚
β”‚  β”‚                β”‚    β”‚                β”‚    β”‚                         β”‚   β”‚
β”‚  β”‚ β€’ LoginUseCase β”‚    β”‚ β€’ Recipe       β”‚    β”‚ β€’ RecipeRepository      β”‚   β”‚
β”‚  β”‚ β€’ GetRecipes   β”‚    β”‚ β€’ Ingredient   β”‚    β”‚ β€’ AuthRepository        β”‚   β”‚
β”‚  β”‚ β€’ ToggleFav    β”‚    β”‚ β€’ Step         β”‚    β”‚                         β”‚   β”‚
β”‚  β”‚ β€’ SearchRecipesβ”‚    β”‚ β€’ LoginResult  β”‚    β”‚                         β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                           β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                            β”‚ implements
                                                            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              DATA LAYER                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Repository    β”‚    β”‚  Data Sources  β”‚    β”‚        Mappers          β”‚   β”‚
β”‚  β”‚     Impl       β”‚    β”‚                β”‚    β”‚                         β”‚   β”‚
β”‚  β”‚                │◄───│ β€’ RecipesApi   β”‚    β”‚ β€’ RecipeEntity β†’ Recipe β”‚   β”‚
β”‚  β”‚ β€’ RecipeRepo   β”‚    β”‚ β€’ RecipeDao    β”‚    β”‚ β€’ RecipeDto β†’ Recipe    β”‚   β”‚
β”‚  β”‚ β€’ AuthRepo     β”‚    β”‚ β€’ AuthApi      β”‚    β”‚                         β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   User   │────►│   Composable │────►│ ViewModel │────►│  Use Case  │────►│ Repository β”‚
β”‚  Action  β”‚     β”‚    Screen    β”‚     β”‚           β”‚     β”‚            β”‚     β”‚            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
                                            β”‚                                      β”‚
                                            β”‚ StateFlow                            β”‚
                                            β”‚                                      β–Ό
                                      β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                      β”‚ UI State  │◄────────────────────────│ Data Source  β”‚
                                      β”‚ (Updated) β”‚       Result<T>         β”‚  (API/Room)  β”‚
                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Module Structure

The project follows a feature-based modularization strategy:

BakingApp/
β”‚
β”œβ”€β”€ πŸ“± app/                              # Application entry point
β”‚   β”œβ”€β”€ BakingApplication.kt             # Hilt Application class
β”‚   β”œβ”€β”€ MainActivity.kt                  # Single Activity
β”‚   └── navigation/
β”‚       └── BakingNavHost.kt             # Navigation graph
β”‚
β”œβ”€β”€ 🧱 core/                             # Shared core modules
β”‚   β”œβ”€β”€ common/                          # Base classes & utilities
β”‚   β”‚   β”œβ”€β”€ base/
β”‚   β”‚   β”‚   └── BaseUseCase.kt           # UseCase, FlowUseCase, NoParamUseCase
β”‚   β”‚   β”œβ”€β”€ result/
β”‚   β”‚   β”‚   └── Result.kt                # Result<T> sealed class
β”‚   β”‚   β”œβ”€β”€ dispatcher/
β”‚   β”‚   β”‚   └── DispatcherModule.kt      # Coroutine dispatchers
β”‚   β”‚   └── extensions/
β”‚   β”‚       └── FlowExtensions.kt        # Flow utility extensions
β”‚   β”‚
β”‚   β”œβ”€β”€ network/                         # Networking layer
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthApi.kt               # Authentication endpoints
β”‚   β”‚   β”‚   └── RecipesApi.kt            # Recipe endpoints
β”‚   β”‚   β”œβ”€β”€ interceptor/
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthInterceptor.kt       # Token injection
β”‚   β”‚   β”‚   └── NetworkDelayInterceptor.kt
β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”œβ”€β”€ RecipeDto.kt             # Data Transfer Objects
β”‚   β”‚   β”‚   └── NetworkResponse.kt       # API response wrapper
β”‚   β”‚   └── di/
β”‚   β”‚       └── NetworkModule.kt         # Hilt network providers
β”‚   β”‚
β”‚   β”œβ”€β”€ database/                        # Local persistence
β”‚   β”‚   β”œβ”€β”€ BakingDatabase.kt            # Room database
β”‚   β”‚   β”œβ”€β”€ dao/
β”‚   β”‚   β”‚   └── RecipeDao.kt             # Recipe data access
β”‚   β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”‚   └── RecipeEntity.kt          # Room entities
β”‚   β”‚   └── di/
β”‚   β”‚       └── DatabaseModule.kt        # Hilt database providers
β”‚   β”‚
β”‚   β”œβ”€β”€ security/                        # Security utilities
β”‚   β”‚   β”œβ”€β”€ cpp/                         # Native code (NDK)
β”‚   β”‚   β”‚   β”œβ”€β”€ CMakeLists.txt           # CMake build config
β”‚   β”‚   β”‚   └── native-keys.cpp          # XOR-obfuscated keys
β”‚   β”‚   β”œβ”€β”€ ApiKeyProvider.kt            # Key provider interface
β”‚   β”‚   β”œβ”€β”€ NativeKeyProvider.kt         # JNI bridge to native
β”‚   β”‚   β”œβ”€β”€ EncryptedPreferencesManager.kt
β”‚   β”‚   β”œβ”€β”€ SecureTokenManager.kt        # Token management
β”‚   β”‚   └── di/
β”‚   β”‚       └── SecurityModule.kt
β”‚   β”‚
β”‚   └── ui/                              # Shared UI components
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ BakingButton.kt
β”‚       β”‚   β”œβ”€β”€ BakingTextField.kt
β”‚       β”‚   β”œβ”€β”€ ErrorView.kt
β”‚       β”‚   β”œβ”€β”€ LoadingIndicator.kt
β”‚       β”‚   └── RecipeCard.kt
β”‚       └── theme/
β”‚           β”œβ”€β”€ Color.kt
β”‚           β”œβ”€β”€ Theme.kt
β”‚           └── Type.kt
β”‚
β”œβ”€β”€ 🎨 features/                         # Feature modules
β”‚   β”œβ”€β”€ login/                           # Authentication feature
β”‚   β”‚   β”œβ”€β”€ data/
β”‚   β”‚   β”‚   └── repository/
β”‚   β”‚   β”‚       └── AuthRepositoryImpl.kt
β”‚   β”‚   β”œβ”€β”€ domain/
β”‚   β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”‚   └── LoginResult.kt
β”‚   β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”‚   └── AuthRepository.kt
β”‚   β”‚   β”‚   └── usecase/
β”‚   β”‚   β”‚       β”œβ”€β”€ LoginUseCase.kt
β”‚   β”‚   β”‚       β”œβ”€β”€ ValidateEmailUseCase.kt
β”‚   β”‚   β”‚       └── ValidatePasswordUseCase.kt
β”‚   β”‚   β”œβ”€β”€ presentation/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginScreen.kt
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginViewModel.kt
β”‚   β”‚   β”‚   └── LoginUiState.kt
β”‚   β”‚   └── di/
β”‚   β”‚       └── LoginModule.kt
β”‚   β”‚
β”‚   β”œβ”€β”€ home/                            # Recipe list feature
β”‚   β”‚   β”œβ”€β”€ data/
β”‚   β”‚   β”‚   β”œβ”€β”€ datasource/
β”‚   β”‚   β”‚   β”‚   └── FakeRecipeDataSource.kt
β”‚   β”‚   β”‚   β”œβ”€β”€ mapper/
β”‚   β”‚   β”‚   β”‚   └── RecipeMapper.kt
β”‚   β”‚   β”‚   └── repository/
β”‚   β”‚   β”‚       └── RecipeRepositoryImpl.kt
β”‚   β”‚   β”œβ”€β”€ domain/
β”‚   β”‚   β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”‚   β”‚   └── Recipe.kt
β”‚   β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”‚   └── RecipeRepository.kt
β”‚   β”‚   β”‚   └── usecase/
β”‚   β”‚   β”‚       β”œβ”€β”€ GetRecipesUseCase.kt
β”‚   β”‚   β”‚       β”œβ”€β”€ SearchRecipesUseCase.kt
β”‚   β”‚   β”‚       └── ToggleFavoriteUseCase.kt
β”‚   β”‚   β”œβ”€β”€ presentation/
β”‚   β”‚   β”‚   β”œβ”€β”€ HomeScreen.kt
β”‚   β”‚   β”‚   β”œβ”€β”€ HomeViewModel.kt
β”‚   β”‚   β”‚   └── HomeUiState.kt
β”‚   β”‚   └── di/
β”‚   β”‚       └── HomeModule.kt
β”‚   β”‚
β”‚   └── recipe-details/                  # Recipe detail feature
β”‚       └── presentation/
β”‚           β”œβ”€β”€ RecipeDetailScreen.kt
β”‚           β”œβ”€β”€ RecipeDetailViewModel.kt
β”‚           └── RecipeDetailUiState.kt
β”‚
└── πŸ“š docs/                             # Documentation
    β”œβ”€β”€ architecture.md
    β”œβ”€β”€ modules.md
    β”œβ”€β”€ networking.md
    β”œβ”€β”€ security.md
    β”œβ”€β”€ testing.md
    β”œβ”€β”€ performance.md
    └── compose_guidelines.md

Module Dependencies

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   app   β”‚
                    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
                         β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚              β”‚              β”‚
          β–Ό              β–Ό              β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   login   β”‚  β”‚   home    β”‚  β”‚ recipe-details β”‚
    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚              β”‚                β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚              β”‚              β”‚
          β–Ό              β–Ό              β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ core:ui  β”‚  β”‚core:commonβ”‚  β”‚core:networkβ”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚core:securityβ”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🎭 MVI Pattern Implementation

UI State

Immutable data class representing the entire screen state:

// LoginUiState.kt
data class LoginUiState(
    val email: String = "",
    val password: String = "",
    val emailError: String? = null,
    val passwordError: String? = null,
    val isLoading: Boolean = false,
    val isLoggedIn: Boolean = false,
    val errorMessage: String? = null
) {
    val isFormValid: Boolean
        get() = email.isNotBlank() && 
                password.isNotBlank() && 
                emailError == null && 
                passwordError == null
}

One-Time Events

Sealed class for navigation and side effects:

sealed class LoginEvent {
    data class NavigateToHome(val userName: String) : LoginEvent()
    data class ShowError(val message: String) : LoginEvent()
}

ViewModel

State holder with intent handling:

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val loginUseCase: LoginUseCase,
    private val validateEmailUseCase: ValidateEmailUseCase,
    private val validatePasswordUseCase: ValidatePasswordUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
    
    private val _events = Channel<LoginEvent>(Channel.BUFFERED)
    val events = _events.receiveAsFlow()
    
    fun onEmailChange(email: String) {
        _uiState.update { state ->
            state.copy(email = email, emailError = null)
        }
    }
    
    fun onLoginClick() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            
            when (val result = loginUseCase(email, password)) {
                is Result.Success -> {
                    _uiState.update { it.copy(isLoading = false, isLoggedIn = true) }
                    _events.send(LoginEvent.NavigateToHome(result.data.name))
                }
                is Result.Error -> {
                    _uiState.update { it.copy(isLoading = false, errorMessage = result.message) }
                }
                is Result.Loading -> { /* handled */ }
            }
        }
    }
}

πŸ”§ Result Type

A generic sealed class for handling async operations:

sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Error(
        val exception: Throwable,
        val message: String? = exception.message
    ) : Result<Nothing>
    data object Loading : Result<Nothing>
}

// Extension functions
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R>
inline fun <T> Result<T>.onSuccess(action: (T) -> Unit): Result<T>
inline fun <T> Result<T>.onError(action: (Throwable, String?) -> Unit): Result<T>
fun <T> Result<T>.getOrNull(): T?
fun <T> Result<T>.getOrDefault(default: T): T
fun <T> Result<T>.getOrThrow(): T

πŸ“ Base Use Cases

Reusable base classes for business logic:

// Single value use case
abstract class UseCase<in P, R>(
    private val coroutineDispatcher: CoroutineDispatcher
) {
    suspend operator fun invoke(parameters: P): Result<R> {
        return try {
            withContext(coroutineDispatcher) {
                Result.Success(execute(parameters))
            }
        } catch (e: Exception) {
            Result.Error(e)
        }
    }
    
    protected abstract suspend fun execute(parameters: P): R
}

// Flow-based use case
abstract class FlowUseCase<in P, R>(
    private val coroutineDispatcher: CoroutineDispatcher
) {
    operator fun invoke(parameters: P): Flow<Result<R>> {
        return execute(parameters)
            .catch { e -> emit(Result.Error(e as Exception)) }
            .flowOn(coroutineDispatcher)
    }
    
    protected abstract fun execute(parameters: P): Flow<Result<R>>
}

// No parameter variants
abstract class NoParamUseCase<R>(dispatcher: CoroutineDispatcher)
abstract class NoParamFlowUseCase<R>(dispatcher: CoroutineDispatcher)

πŸ› οΈ Tech Stack

Core

Technology Version Purpose
Kotlin 2.0.20 Programming language
Jetpack Compose 2025.09.00 Declarative UI toolkit
Material 3 Latest Design system
Hilt 2.52 Dependency injection
Coroutines 1.9.0 Asynchronous programming
Navigation Compose 2.9.1 Screen navigation

Networking

Technology Version Purpose
Retrofit 2.11.0 HTTP client
OkHttp 4.12.0 HTTP engine + interceptors
Moshi 1.15.0 JSON serialization

Database & Storage

Technology Version Purpose
Room 2.6.1 Local SQLite database
Paging 3 3.3.6 Efficient data pagination
EncryptedSharedPreferences 1.1.0-alpha06 Secure key-value storage

Background Processing

Technology Version Purpose
WorkManager 2.10.4 Background task scheduling

Image Loading

Technology Version Purpose
Coil 2.6.0 Image loading for Compose

Testing

Technology Version Purpose
JUnit 4.13.2 Unit test framework
Turbine 1.2.1 Flow testing
MockWebServer 4.12.0 API mocking
Truth 1.4.5 Fluent assertions
Mockito 5.12.0 Mocking framework
Espresso 3.7.0 UI testing

πŸš€ Getting Started

Prerequisites

  • Android Studio Hedgehog (2023.1.1) or newer
  • JDK 17
  • Android SDK 35 (minimum SDK 24)

Build & Run

# Clone the repository
git clone https://github.com/your-repo/baking-app.git
cd baking-app

# Build debug APK
./gradlew assembleDebug

# Install on connected device
./gradlew installDebug

# Or simply open in Android Studio and click Run ▢️

Test Credentials

Field Value
Email [email protected]
Password Password123

πŸ§ͺ Testing

The project includes comprehensive tests across all layers:

Unit Tests

# Run all unit tests
./gradlew testDebugUnitTest

# Run specific module tests
./gradlew :features:login:testDebugUnitTest
./gradlew :features:home:testDebugUnitTest

Integration Tests

# Run instrumented tests
./gradlew connectedAndroidTest

Test Coverage

Layer Test Types
Domain Use case tests with fake repositories
Data Repository tests with MockWebServer
Presentation ViewModel tests with Turbine
UI Compose UI tests with Espresso

Example Test

@Test
fun `login with valid credentials returns success`() = runTest {
    // Given
    val fakeRepository = FakeAuthRepository()
    val loginUseCase = LoginUseCase(fakeRepository)
    
    // When
    val result = loginUseCase("[email protected]", "Password123")
    
    // Then
    assertThat(result).isInstanceOf(Result.Success::class.java)
}

πŸ”’ Security Features

Feature Implementation
Native API Key Storage NDK/C++ with XOR obfuscation for API keys
Encrypted Storage EncryptedSharedPreferences for tokens
No Sensitive Logs ProGuard rules remove logging in release
Certificate Pinning Ready for production configuration
Clear-text Disabled Network security config enforces HTTPS
Code Obfuscation R8 minification for release builds

πŸ”‘ Native Key Provider

API keys are stored securely in native C++ code with multiple protection layers:

@Inject
lateinit var apiKeyProvider: ApiKeyProvider

// Get API key from native storage
val apiKey = apiKeyProvider.getApiKey()

Security Layers:

  • πŸ›‘οΈ Native Code - Compiled to ARM/x86 assembly (hard to decompile)
  • πŸ” XOR Obfuscation - Keys not visible in hex editors
  • πŸ“¦ Package Verification - Keys only work with correct package name
  • βœ‚οΈ String Splitting - No complete key in one location

See security.md for detailed implementation guide.


⚑ Performance Optimizations

Optimization Benefit
Immutable UI State Prevents unintended state mutations
Stable Composables Efficient recomposition
Proper Coroutine Scoping No memory leaks
Database Indices Fast query performance
Image Caching Reduced network calls with Coil
Offline-First Instant data from local cache

πŸ“± Navigation

App Navigation Graph
β”‚
β”œβ”€β”€ /login         β†’  Authentication screen (Start destination)
β”‚                     β”œβ”€β”€ Email input
β”‚                     β”œβ”€β”€ Password input
β”‚                     └── Login button
β”‚
β”œβ”€β”€ /home          β†’  Recipes list screen
β”‚                     β”œβ”€β”€ Search bar
β”‚                     β”œβ”€β”€ Category chips
β”‚                     └── Recipe grid
β”‚
└── /recipe/{id}   β†’  Recipe detail screen
                      β”œβ”€β”€ Hero image
                      β”œβ”€β”€ Ingredients
                      └── Steps

🎨 Design Principles

SOLID Principles Applied

Principle Application
Single Responsibility Each class has one job: ViewModels manage UI state, UseCases contain business logic
Open/Closed Repository interfaces allow extension without modification
Liskov Substitution FakeRepository seamlessly replaces real implementation in tests
Interface Segregation Small, focused interfaces (TokenProvider vs AuthManager)
Dependency Inversion High-level modules depend on abstractions, not implementations

Domain Models

data class Recipe(
    val id: String,
    val name: String,
    val description: String,
    val imageUrl: String?,
    val servings: Int,
    val prepTimeMinutes: Int,
    val cookTimeMinutes: Int,
    val difficulty: Difficulty,
    val category: String,
    val isFavorite: Boolean = false,
    val ingredients: List<Ingredient> = emptyList(),
    val steps: List<Step> = emptyList()
) {
    val totalTimeMinutes: Int
        get() = prepTimeMinutes + cookTimeMinutes
}

enum class Difficulty {
    EASY, MEDIUM, HARD;
    
    fun toDisplayString(): String = when (this) {
        EASY -> "Easy"
        MEDIUM -> "Medium"
        HARD -> "Hard"
    }
}

πŸ“š Documentation

Detailed documentation is available in the /docs folder:

Document Description
architecture.md Clean Architecture deep dive
modules.md Module structure and dependencies
networking.md Network layer implementation
security.md Security: NDK keys, encryption, network
testing.md Testing strategy and examples
performance.md Performance optimization guide
compose_guidelines.md Jetpack Compose best practices
interview_questions.md Senior Android interview Q&A

πŸ§‘β€πŸ’» Contributing

We welcome contributions! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch
    git checkout -b feature/amazing-feature
  3. Commit your changes
    git commit -m 'Add amazing feature'
  4. Push to the branch
    git push origin feature/amazing-feature
  5. Open a Pull Request

Code Style


πŸ“„ License

MIT License

Copyright (c) 2024 BakingApp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

πŸ™ Acknowledgments


Made with ❀️ using Clean Architecture

⭐ Star this repo if you find it helpful!

About

🍰 Production-ready Android app demonstrating Clean Architecture with multi-module setup. Built with Jetpack Compose, Material 3, MVI pattern, Hilt DI, Retrofit, Room, and Kotlin Coroutines. Perfect for learning enterprise Android development best practices.

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages