← Back to Blog
Mobile

Clean Architecture in Android: Lessons from Production

Apr 202612 min read

Clean Architecture sounds intimidating — UseCases, Repositories, Mappers everywhere. But after shipping multiple production Android apps, I've found a simplified version that keeps code maintainable without the ceremony.

The 3 Layers That Matter

The core idea is dependency direction: outer layers depend on inner layers, never the reverse. In practice for Android: Presentation → Domain → Data.

  • Data layer: Retrofit/Room implementations, remote & local data sources, Repository implementations
  • Domain layer: Repository interfaces, UseCase classes, plain Kotlin models
  • Presentation layer: ViewModels, Compose UI, UiState sealed classes

The UseCase Pattern

UseCases are single-responsibility classes that encapsulate one piece of business logic. They're easy to test and make your ViewModel clean.

// Domain layer — pure Kotlin, no Android imports
class GetUserProfileUseCase(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): Result<UserProfile> {
        return userRepository.getUserProfile(userId)
    }
}

// ViewModel just orchestrates
class ProfileViewModel(
    private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
    fun loadProfile(id: String) = viewModelScope.launch {
        _uiState.value = getUserProfile(id).fold(
            onSuccess = { UiState.Success(it) },
            onFailure = { UiState.Error(it.message) }
        )
    }
}

Common Pitfalls to Avoid

  • Don't create a UseCase for every single CRUD operation — group related operations
  • Don't put business logic in Composables or ViewModels — it belongs in UseCases
  • Don't skip the domain layer — it's what makes your code testable

💡 In Luna Owner, we rebuilt the entire app with this architecture pattern. Result: test coverage jumped from 0% to 65%, and feature development became measurably faster.

Conclusion

Start with the 3-layer structure. Add UseCases where business logic is non-trivial. Keep Composables dumb — they should only transform UiState into UI. That's it.