Clean Architecture
1. Giới thiệu
Phần tiêu đề “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
Phần tiêu đề “2. Layers”┌─────────────────────────────────────────────┐│ Presentation Layer ││ (UI, ViewModels, Compose) │├─────────────────────────────────────────────┤│ Domain Layer ││ (UseCases, Entities) │├─────────────────────────────────────────────┤│ Data Layer ││ (Repositories, DataSources, DTOs) │└─────────────────────────────────────────────┘Dependency Rule
Phần tiêu đề “Dependency Rule”Dependencies chỉ point inward:
- Presentation → Domain
- Data → Domain
- Domain không phụ thuộc gì
3. Domain Layer
Phần tiêu đề “3. Domain Layer”Entities
Phần tiêu đề “Entities”// Pure Kotlin, no Android dependenciesdata class User( val id: Int, val name: String, val email: String)Repository Interface
Phần tiêu đề “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
Phần tiêu đề “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 logicclass 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
Phần tiêu đề “4. Data Layer”DTOs (Data Transfer Objects)
Phần tiêu đề “DTOs (Data Transfer Objects)”data class UserDto( @SerializedName("id") val id: Int, @SerializedName("user_name") val name: String, @SerializedName("email_address") val email: String)
// Mapperfun UserDto.toDomain(): User = User(id, name, email)fun User.toDto(): UserDto = UserDto(id, name, email)Data Sources
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “5. Presentation Layer”ViewModel
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “8. Testing”// Test UseCase@Testfun `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@Testfun `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
Phần tiêu đề “📝 Tóm tắt”| Layer | Contains | Dependencies |
|---|---|---|
| Domain | UseCases, Entities | None (pure Kotlin) |
| Data | Repositories, DTOs | Domain |
| Presentation | UI, ViewModels | Domain |
Benefits
Phần tiêu đề “Benefits”- Testable
- Maintainable
- Scalable
- Framework independent (Domain)