1 / 18

๐Ÿ›๏ธ Android Clean Architecture

A Complete Beginner's Guide

๐ŸŽฏ What You'll Master:

  • Understanding Clean Architecture principles
  • Layer separation and responsibilities
  • Dependency rules and flow
  • Real Android implementation examples
  • Best practices and common patterns

๐Ÿค” What is Clean Architecture?

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

๐Ÿงช

Testable

Easy to write unit tests

๐Ÿ”ง

Maintainable

Easy to modify and extend

๐Ÿ”’

Independent

Not tied to specific frameworks

๐Ÿ˜ต Problems Without Clean Architecture

โŒ Traditional Approach

  • UI mixed with business logic
  • Hard to test components
  • Tightly coupled code
  • Database changes break everything
  • Difficult to add new features

โœ… Clean Architecture

  • Clear separation of concerns
  • Easy unit testing
  • Loosely coupled components
  • Database independence
  • Feature additions are simple

๐Ÿ—๏ธ The Three Main Layers

๐Ÿ“ฑ PRESENTATION LAYER

UI, Activities, Fragments, ViewModels

โฌ‡๏ธ

๐Ÿ’ผ DOMAIN LAYER

Business Logic, Use Cases, Entities

โฌ‡๏ธ

๐Ÿ’พ DATA LAYER

Repositories, Data Sources, APIs, Database

๐Ÿ“ The Dependency Rule

Most Important Rule: Dependencies can only point inward!

Presentation
โ†’
Domain
โ†’
Data

โœ… Allowed:

  • Presentation โ†’ Domain
  • Data โ†’ Domain

โŒ Not Allowed:

  • Domain โ†’ Presentation
  • Domain โ†’ Data

๐Ÿ’พ Data Layer - Deep Dive

Responsibilities:

// Data Layer Components
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()
        }
    }
}

๐Ÿ’ผ Domain Layer - Deep Dive

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)
        }
    }
}

๐Ÿ“ฑ Presentation Layer - Deep Dive

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 Explained

Use Cases represent specific business operations

One Use Case = One Business Operation

Examples: Login User, Get User Profile, Update Settings, Send Message

๐ŸŽฏ

Single Responsibility

Each use case has one clear purpose

๐Ÿ”„

Reusable

Can be used from multiple UI components

๐Ÿงช

Testable

Easy to test business logic in isolation

๐Ÿ’‰ Dependency Injection

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)
    }
}

๐Ÿ”„ Complete Data Flow Example

1. User Action: User taps "Load Users" button in Activity
2. Presentation: Activity calls ViewModel.loadUsers()
3. Domain: ViewModel calls GetUsersUseCase.invoke()
4. Data: Use case calls UserRepository.getUsers()
5. Network/DB: Repository fetches data from API/Database
6. Return Data: Data flows back through the layers
7. Update UI: ViewModel updates UI state, Activity observes and updates UI

๐Ÿ“š Repository Pattern

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()
        }
    }
}

โš ๏ธ Error Handling in Clean Architecture

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

๐Ÿงช Testing Made Easy

Clean Architecture makes testing straightforward

๐ŸŽฏ

Unit Tests

Test use cases and business logic independently

๐Ÿ”ง

Integration Tests

Test repository implementations with mock data sources

๐Ÿ“ฑ

UI Tests

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())
}

๐Ÿ“ Project Structure

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

โŒ Common Mistakes to Avoid

โŒ Don't Do This

  • Put Android dependencies in Domain layer
  • Let Domain layer know about UI
  • Create "God" use cases
  • Skip dependency injection
  • Mix business logic in ViewModels

โœ… Do This Instead

  • Keep Domain layer pure Kotlin
  • Use abstractions and interfaces
  • Create focused, single-purpose use cases
  • Implement proper DI
  • Keep ViewModels as thin coordinators

๐ŸŽ‰ Clean Architecture Benefits

๐Ÿงช

Testability

Easy to write and maintain tests at all levels

๐Ÿ”ง

Maintainability

Changes in one layer don't affect others

๐Ÿš€

Scalability

Easy to add new features and team members

๐Ÿ”„

Flexibility

Easy to change databases, APIs, or UI frameworks

๐Ÿ‘ฅ

Team Collaboration

Multiple developers can work on different layers

๐Ÿ“š

Code Reuse

Use cases can be shared across different UIs

๐Ÿš€ Ready to Get Started?

1. Start Small: Begin with a simple feature like user login
2. Create Layers: Set up your folder structure first
3. Define Entities: Start with your domain models
4. Create Use Cases: Implement your business logic
5. Build Repository: Abstract your data access
6. Setup DI: Use Hilt or Dagger for dependency injection
7. Connect UI: Wire everything together in your ViewModels

๐ŸŽฏ Pro Tips:

  • Don't over-engineer - start simple and refactor
  • Focus on the dependency rule above all
  • Write tests as you go
  • Use existing libraries that follow Clean Architecture

Happy Clean Coding! ๐ŸŽ‰