A Complete Beginner's Guide
Clean Architecture is a software design approach that separates concerns into independent layers.
Created by: Robert C. Martin (Uncle Bob)
Goal: Make software independent of frameworks, UI, databases, and external agencies
Easy to write unit tests
Easy to modify and extend
Not tied to specific frameworks
UI, Activities, Fragments, ViewModels
Business Logic, Use Cases, Entities
Repositories, Data Sources, APIs, Database
Most Important Rule: Dependencies can only point inward!
Responsibilities:
interface UserRepository { suspend fun getUsers(): List<User> suspend fun getUserById(id: Int): User? } class UserRepositoryImpl( private val apiService: UserApiService, private val userDao: UserDao ) : UserRepository { override suspend fun getUsers(): List<User> { return try { val users = apiService.getUsers() userDao.insertUsers(users) users } catch (e: Exception) { userDao.getAllUsers() } } }
The Heart of Your App:
// Domain Layer Components data class User( val id: Int, val name: String, val email: String ) class GetUsersUseCase( private val userRepository: UserRepository ) { suspend operator fun invoke(): Result<List<User>> { return try { val users = userRepository.getUsers() Result.success(users) } catch (e: Exception) { Result.failure(e) } } }
User Interface Components:
// Presentation Layer Components class UserViewModel( private val getUsersUseCase: GetUsersUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState = _uiState.asStateFlow() fun loadUsers() { viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true) getUsersUseCase().fold( onSuccess = { users -> _uiState.value = _uiState.value.copy( users = users, isLoading = false ) }, onFailure = { error -> _uiState.value = _uiState.value.copy( error = error.message, isLoading = false ) } ) } } }
Use Cases represent specific business operations
One Use Case = One Business Operation
Examples: Login User, Get User Profile, Update Settings, Send Message
Each use case has one clear purpose
Can be used from multiple UI components
Easy to test business logic in isolation
DI helps us follow the dependency rule
// Without DI (Bad) class UserViewModel { private val repository = UserRepositoryImpl() // Hard dependency } // With DI (Good) class UserViewModel( private val getUsersUseCase: GetUsersUseCase // Injected dependency ) : ViewModel() // DI Module (using Hilt) @Module @InstallIn(ViewModelComponent::class) object DomainModule { @Provides fun provideGetUsersUseCase( userRepository: UserRepository ): GetUsersUseCase { return GetUsersUseCase(userRepository) } }
Repository abstracts data access logic
Benefits: Single source of truth, caching logic, data source switching
interface UserRepository { suspend fun getUsers(): List<User> suspend fun getUserById(id: Int): User? suspend fun saveUser(user: User) } class UserRepositoryImpl( private val remoteDataSource: UserRemoteDataSource, private val localDataSource: UserLocalDataSource ) : UserRepository { override suspend fun getUsers(): List<User> { return if (isNetworkAvailable()) { val users = remoteDataSource.getUsers() localDataSource.cacheUsers(users) users } else { localDataSource.getCachedUsers() } } }
Each layer handles errors appropriately
// Domain Layer - Use Case with Result wrapper class GetUsersUseCase( private val userRepository: UserRepository ) { suspend operator fun invoke(): Result<List<User>> { return try { val users = userRepository.getUsers() if (users.isEmpty()) { Result.failure(EmptyDataException()) } else { Result.success(users) } } catch (e: NetworkException) { Result.failure(NetworkErrorException()) } catch (e: Exception) { Result.failure(UnknownErrorException()) } } }
Best Practice: Use sealed classes or Result wrappers to handle success/error states
Clean Architecture makes testing straightforward
Test use cases and business logic independently
Test repository implementations with mock data sources
Test presentation layer with mocked use cases
// Testing Use Case @Test fun `when getUsersUseCase is called, then return users list`() = runTest { // Given val mockUsers = listOf(User(1, "John", "john@email.com")) val mockRepository = mockk<UserRepository>() coEvery { mockRepository.getUsers() } returns mockUsers val useCase = GetUsersUseCase(mockRepository) // When val result = useCase() // Then assertTrue(result.isSuccess) assertEquals(mockUsers, result.getOrNull()) }
app/ โโโ presentation/ โ โโโ ui/ โ โ โโโ activities/ โ โ โโโ fragments/ โ โ โโโ adapters/ โ โโโ viewmodels/ โ โโโ di/ โโโ domain/ โ โโโ entities/ โ โโโ usecases/ โ โโโ repositories/ (interfaces) โโโ data/ โโโ repositories/ (implementations) โโโ datasources/ โ โโโ remote/ โ โโโ local/ โโโ database/ โโโ network/
Alternative: You can also organize by features (user/, product/, order/) with each having its own presentation/domain/data folders
Easy to write and maintain tests at all levels
Changes in one layer don't affect others
Easy to add new features and team members
Easy to change databases, APIs, or UI frameworks
Multiple developers can work on different layers
Use cases can be shared across different UIs
Happy Clean Coding! ๐