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.kt7. 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
| Layer | Contains | Dependencies |
|---|---|---|
| Domain | UseCases, Entities | None (pure Kotlin) |
| Data | Repositories, DTOs | Domain |
| Presentation | UI, ViewModels | Domain |
Benefits
- Testable
- Maintainable
- Scalable
- Framework independent (Domain)
Last updated on