Android Clean Architecture Excellence 🏗️
Standardized architectural patterns for scalable, testable, and maintainable Android applications.
⚡ When to Use
- App Structure: Planning or refactoring the module/package layout.
- Data Fetching: Implementing Retrofit or Room repositories.
- Business Logic: Writing Use Cases or Interactors.
- UI Architecture: Choosing between MVI, MVVM, or custom patterns.
- State Management: Consuming data from Flow/StateFlow in ViewModels.
🏗️ The Three Pillars
1. Presentation Layer (UI & ViewModels)
- Pattern: Always use Jetpack ViewModel.
- State: Use
MutableStateFlowin ViewModel. In Compose UI, ALWAYS collect usingcollectAsStateWithLifecycle()(requiresandroidx.lifecycle:lifecycle-runtime-compose). NEVER usecollectAsState()as it wastes resources in the background. - MVI/MVVM: Prefer State + Events + Effects for complex screens.
- Anti-Pattern (No Stubs): NEVER generate stubbed empty callbacks (e.g.,
onClicked = { /* Future implementation */ }) when scaffolding. If tasked to build a feature, ALWAYS implement the full event flow from UI to ViewModel to Repository. - Navigation: Use Compose Navigation with Safe Args (Type-safe routing via Kotlinx Serialization) instead of manual screen swapping.
2. Domain Layer (The Core)
Rule: NO Android dependencies (except maybe
@Inject). Pure Kotlin.Use Cases: Each Use Case should have a single responsibility.
class GetUserUseCase @Inject constructor(private val repository: UserRepository) { operator fun invoke(id: String): Flow<User> = repository.getUser(id) }Models: Pure Kotlin data classes (Domain-specific).
CRITICAL ANTI-HALLUCINATION GUARD: NEVER assume or guess the properties of a data class (e.g., guessing a
TodoItemhas adueDate). You MUST explicitly read the data classes indomain/model/before writing ViewModel or Repository mapping logic to preventUnresolved referenceor constructor errors.
3. Data Layer (Sources & Repositories)
- Pattern: Repository as a single source of truth.
- DTOs: Data Transfer Objects (e.g., Json objects from Retrofit). Always map DTOs to Domain Models before passing to the Domain Layer.
- Sources: Remote (Retrofit/Ktor) and Local (Room/DataStore).
- Room TypeConverters (CRITICAL): If your domain model contains non-primitive types (e.g.,
LocalDateTime, Enums, Custom Objects), you MUST create a@TypeConverterclass and explicitly register it on the@Databaseclass before compiling. Room cannot natively store them!
💉 Dependency Injection (Hilt)
Hilt is the standard DI library. Ensure the following foundational setup is ALWAYS present:
1. Mandatory Application Class
- Rule: Create a class inheriting from
Applicationand annotate it with@HiltAndroidApp.@HiltAndroidApp class BaseApplication : Application()
2. Manifest Registration
- Rule: Register your application class in
AndroidManifest.xmlusingandroid:name=".BaseApplication".
3. Usage Patterns
- Use
@HiltViewModelfor ViewModels. - Use
@AndroidEntryPointfor Activities/Fragments (if any). - Use
@Inject constructorfor Use Cases and Repositories. - Create
@Moduleand@InstallIn(SingletonComponent::class)for platform dependencies.
🧪 Testing Strategy
- Unit Tests: Place in
test/. Use MockK for mocking interfaces. - Test Coroutines: Use
runTestandTestDispatcher. - Fakes vs Mocks: Prefer Fakes for complex Data sources and Mocks for simple verification.
🚀 Modern Reactive Flows
- StateFlow: For state that needs initial value and persistence.
- SharedFlow: For one-off events (e.g., Navigation, Snackbar).
- Error Handling: Use a
Resultwrapper orResourceobject to encapsulate Success/Error states.