Enterprise Android Clean Architecture Sample
A production-ready, scalable Android application showcasing modern development practices, clean architecture, and best-in-class libraries.
Features β’ Architecture β’ Tech Stack β’ Getting Started β’ Documentation
| ποΈ 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 |
| Login | Home | Recipe Details |
|---|---|---|
| π Secure Authentication | π Recipe Discovery | π Step-by-Step Guide |
- Email/password validation with real-time feedback
- Secure token storage using EncryptedSharedPreferences
- Loading states with smooth animations
- Comprehensive error handling
- Beautiful recipe cards with images
- Pull-to-refresh functionality
- Category-based filtering
- Debounced search for performance
- Favorite toggle with local persistence
- Complete ingredients list with quantities
- Step-by-step cooking instructions
- Difficulty badges (Easy, Medium, Hard)
- Prep/cook time information
- Video support for cooking steps
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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β β β β
β ββββββββββββββββββ ββββββββββββββββββ βββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββ ββββββββββββββββ βββββββββββββ ββββββββββββββ ββββββββββββββ
β User ββββββΊβ Composable ββββββΊβ ViewModel ββββββΊβ Use Case ββββββΊβ Repository β
β Action β β Screen β β β β β β β
ββββββββββββ ββββββββββββββββ βββββββ¬ββββββ ββββββββββββββ ββββββββ¬ββββββ
β β
β StateFlow β
β βΌ
βββββββΌββββββ ββββββββββββββββ
β UI State βββββββββββββββββββββββββββ Data Source β
β (Updated) β Result<T> β (API/Room) β
βββββββββββββ ββββββββββββββββ
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
βββββββββββ
β app β
ββββββ¬βββββ
β
ββββββββββββββββΌβββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββββββ
β login β β home β β recipe-details β
βββββββ¬ββββββ βββββββ¬ββββββ βββββββββ¬ββββββββ
β β β
ββββββββββββββββ΄βββββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ βββββββββββββ ββββββββββββ
β core:ui β βcore:commonβ βcore:networkβ
ββββββββββββ βββββββββββββ ββββββββββββ
β
βΌ
ββββββββββββββ
βcore:securityβ
ββββββββββββββ
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
}Sealed class for navigation and side effects:
sealed class LoginEvent {
data class NavigateToHome(val userName: String) : LoginEvent()
data class ShowError(val message: String) : LoginEvent()
}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 */ }
}
}
}
}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(): TReusable 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)| 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 |
| Technology | Version | Purpose |
|---|---|---|
| Retrofit | 2.11.0 | HTTP client |
| OkHttp | 4.12.0 | HTTP engine + interceptors |
| Moshi | 1.15.0 | JSON serialization |
| 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 |
| Technology | Version | Purpose |
|---|---|---|
| WorkManager | 2.10.4 | Background task scheduling |
| Technology | Version | Purpose |
|---|---|---|
| Coil | 2.6.0 | Image loading for Compose |
| 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 |
- Android Studio Hedgehog (2023.1.1) or newer
- JDK 17
- Android SDK 35 (minimum SDK 24)
# 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 βΆοΈ| Field | Value |
|---|---|
[email protected] |
|
| Password | Password123 |
The project includes comprehensive tests across all layers:
# Run all unit tests
./gradlew testDebugUnitTest
# Run specific module tests
./gradlew :features:login:testDebugUnitTest
./gradlew :features:home:testDebugUnitTest# Run instrumented tests
./gradlew connectedAndroidTest| 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 |
@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)
}| 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 |
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.
| 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 |
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
| 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 |
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"
}
}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 |
We welcome contributions! Please follow these steps:
- Fork the repository
- Create a feature branch
git checkout -b feature/amazing-feature
- Commit your changes
git commit -m 'Add amazing feature' - Push to the branch
git push origin feature/amazing-feature
- Open a Pull Request
- Follow Kotlin coding conventions
- Use meaningful commit messages
- Add tests for new features
- Update documentation as needed
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.
- Google Android Team for Jetpack libraries
- Unsplash for sample images
- Material Design for design guidelines
- The Android community for inspiration and best practices
Made with β€οΈ using Clean Architecture
β Star this repo if you find it helpful!