Skip to Content

Clean Architecture trong Android

1. Giới thiệu

Clean Architecture tách ứng dụng thành các layers độc lập, dễ test và maintain.

2. Layers

┌─────────────────────────────────────────────┐ │ Presentation Layer │ │ (UI, ViewModels, Compose) │ ├─────────────────────────────────────────────┤ │ Domain Layer │ │ (UseCases, Entities) │ ├─────────────────────────────────────────────┤ │ Data Layer │ │ (Repositories, DataSources, DTOs) │ └─────────────────────────────────────────────┘

Dependency Rule

Dependencies chỉ point inward:

  • Presentation → Domain
  • Data → Domain
  • Domain không phụ thuộc gì

3. Domain Layer

Entities

// Pure Kotlin, no Android dependencies data class User( val id: Int, val name: String, val email: String )

Repository Interface

interface UserRepository { suspend fun getUsers(): Result<List<User>> suspend fun getUser(id: Int): Result<User> suspend fun createUser(user: User): Result<User> }

Use Cases

class GetUsersUseCase( private val repository: UserRepository ) { suspend operator fun invoke(): Result<List<User>> { return repository.getUsers() } } class GetUserUseCase( private val repository: UserRepository ) { suspend operator fun invoke(userId: Int): Result<User> { return repository.getUser(userId) } } // UseCase với business logic class ValidateAndCreateUserUseCase( private val repository: UserRepository ) { suspend operator fun invoke(name: String, email: String): Result<User> { if (name.isBlank()) { return Result.failure(ValidationException("Name is required")) } if (!email.isValidEmail()) { return Result.failure(ValidationException("Invalid email")) } return repository.createUser(User(0, name, email)) } }

4. Data Layer

DTOs (Data Transfer Objects)

data class UserDto( @SerializedName("id") val id: Int, @SerializedName("user_name") val name: String, @SerializedName("email_address") val email: String ) // Mapper fun UserDto.toDomain(): User = User(id, name, email) fun User.toDto(): UserDto = UserDto(id, name, email)

Data Sources

interface UserRemoteDataSource { suspend fun getUsers(): List<UserDto> suspend fun getUser(id: Int): UserDto } interface UserLocalDataSource { suspend fun getUsers(): List<UserEntity> suspend fun saveUsers(users: List<UserEntity>) }

Repository Implementation

class UserRepositoryImpl( private val remoteDataSource: UserRemoteDataSource, private val localDataSource: UserLocalDataSource ) : UserRepository { override suspend fun getUsers(): Result<List<User>> { return try { val users = remoteDataSource.getUsers() localDataSource.saveUsers(users.map { it.toEntity() }) Result.success(users.map { it.toDomain() }) } catch (e: Exception) { // Fallback to cache val cached = localDataSource.getUsers() if (cached.isNotEmpty()) { Result.success(cached.map { it.toDomain() }) } else { Result.failure(e) } } } }

5. Presentation Layer

ViewModel

class UsersViewModel( private val getUsersUseCase: GetUsersUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(UsersUiState()) val uiState: StateFlow<UsersUiState> = _uiState.asStateFlow() init { loadUsers() } private fun loadUsers() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } getUsersUseCase() .onSuccess { users -> _uiState.update { it.copy(isLoading = false, users = users) } } .onFailure { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } } } } }

6. Package Structure

com.example.app/ ├── data/ │ ├── local/ │ │ ├── dao/ │ │ │ └── UserDao.kt │ │ └── entity/ │ │ └── UserEntity.kt │ ├── remote/ │ │ ├── api/ │ │ │ └── ApiService.kt │ │ └── dto/ │ │ └── UserDto.kt │ ├── mapper/ │ │ └── UserMapper.kt │ └── repository/ │ └── UserRepositoryImpl.kt ├── domain/ │ ├── model/ │ │ └── User.kt │ ├── repository/ │ │ └── UserRepository.kt │ └── usecase/ │ ├── GetUsersUseCase.kt │ └── GetUserUseCase.kt └── presentation/ ├── users/ │ ├── UsersScreen.kt │ ├── UsersViewModel.kt │ └── UsersUiState.kt └── theme/ └── Theme.kt

7. Dependency Injection

@Module @InstallIn(SingletonComponent::class) object DataModule { @Provides @Singleton fun provideUserRepository( remoteDataSource: UserRemoteDataSource, localDataSource: UserLocalDataSource ): UserRepository = UserRepositoryImpl(remoteDataSource, localDataSource) } @Module @InstallIn(ViewModelComponent::class) object UseCaseModule { @Provides fun provideGetUsersUseCase( repository: UserRepository ): GetUsersUseCase = GetUsersUseCase(repository) }

8. Testing

// Test UseCase @Test fun `GetUsersUseCase returns users`() = runTest { val mockRepository = mockk<UserRepository>() val users = listOf(User(1, "Alice", "alice@email.com")) coEvery { mockRepository.getUsers() } returns Result.success(users) val useCase = GetUsersUseCase(mockRepository) val result = useCase() assertTrue(result.isSuccess) assertEquals(users, result.getOrNull()) } // Test ViewModel @Test fun `ViewModel loads users on init`() = runTest { val mockUseCase = mockk<GetUsersUseCase>() val users = listOf(User(1, "Alice", "alice@email.com")) coEvery { mockUseCase() } returns Result.success(users) val viewModel = UsersViewModel(mockUseCase) assertEquals(users, viewModel.uiState.value.users) }

📝 Tóm tắt

LayerContainsDependencies
DomainUseCases, EntitiesNone (pure Kotlin)
DataRepositories, DTOsDomain
PresentationUI, ViewModelsDomain

Benefits

  • Testable
  • Maintainable
  • Scalable
  • Framework independent (Domain)
Last updated on