MVVM Pattern
1. Giới thiệu
Phần tiêu đề “1. Giới thiệu”MVVM (Model-View-ViewModel) là architecture pattern được Google khuyến nghị cho Android apps.
2. Components
Phần tiêu đề “2. Components”flowchart LR subgraph View["📱 View (Composable)"] V1["Observes State"] end
subgraph ViewModel["🧠 ViewModel"] VM1["Holds State<br/>Handles Logic"] end
subgraph Model["📦 Model (Repository)"] M1["Data Source<br/>(API, DB)"] end
View -->|"Events"| ViewModel ViewModel -->|"State"| View ViewModel -->|"Request"| Model Model -->|"Data"| ViewModel
style View fill:#e3f2fd,stroke:#2196f3 style ViewModel fill:#fff3e0,stroke:#ff9800 style Model fill:#e8f5e9,stroke:#4caf503. Model Layer
Phần tiêu đề “3. Model Layer”// Data classdata class User( val id: Int, val name: String, val email: String)
// Repositoryclass UserRepository( private val api: ApiService, private val dao: UserDao) { suspend fun getUsers(): Result<List<User>> { return try { val users = api.getUsers() dao.insertAll(users) // Cache locally Result.success(users) } catch (e: Exception) { // Return cached data on error val cached = dao.getAllUsers() if (cached.isNotEmpty()) { Result.success(cached) } else { Result.failure(e) } } }}4. ViewModel Layer
Phần tiêu đề “4. ViewModel Layer”data class UsersUiState( val isLoading: Boolean = false, val users: List<User> = emptyList(), val error: String? = null)
class UsersViewModel( private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(UsersUiState()) val uiState: StateFlow<UsersUiState> = _uiState.asStateFlow()
init { loadUsers() }
fun loadUsers() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) }
repository.getUsers() .onSuccess { users -> _uiState.update { it.copy(isLoading = false, users = users) } } .onFailure { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } } } }
fun refresh() = loadUsers()}5. View Layer (Compose)
Phần tiêu đề “5. View Layer (Compose)”@Composablefun UsersScreen( viewModel: UsersViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState()
UsersContent( state = uiState, onRefresh = viewModel::refresh )}
@Composablefun UsersContent( state: UsersUiState, onRefresh: () -> Unit) { Box(modifier = Modifier.fillMaxSize()) { when { state.isLoading -> { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } state.error != null -> { Column( modifier = Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally ) { Text("Error: ${state.error}") Button(onClick = onRefresh) { Text("Retry") } } } else -> { LazyColumn { items(state.users) { user -> UserItem(user) } } } } }}6. Unidirectional Data Flow
Phần tiêu đề “6. Unidirectional Data Flow”flowchart TD UA["👆 User Action"] --> VM["🧠 ViewModel"] VM --> REPO["📦 Repository"] REPO --> DS["💾 Data Source"] DS --> REPO REPO --> VM VM --> STATE["📊 UI State"] STATE --> UI["🔄 Recompose UI"]
style UA fill:#e3f2fd,stroke:#2196f3 style VM fill:#fff3e0,stroke:#ff9800 style STATE fill:#e8f5e9,stroke:#4caf50 style UI fill:#f3e5f5,stroke:#9c27b0// Events từ UIsealed class UserEvent { object Refresh : UserEvent() data class SearchQueryChanged(val query: String) : UserEvent() data class UserClicked(val userId: Int) : UserEvent()}
class UsersViewModel(...) : ViewModel() {
fun onEvent(event: UserEvent) { when (event) { is UserEvent.Refresh -> loadUsers() is UserEvent.SearchQueryChanged -> search(event.query) is UserEvent.UserClicked -> navigateToDetail(event.userId) } }}
// UsageUsersScreen( onEvent = viewModel::onEvent)7. Navigation Events
Phần tiêu đề “7. Navigation Events”class UsersViewModel(...) : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>() val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
fun onUserClicked(userId: Int) { viewModelScope.launch { _navigationEvent.emit(NavigationEvent.ToUserDetail(userId)) } }}
sealed class NavigationEvent { data class ToUserDetail(val userId: Int) : NavigationEvent() object Back : NavigationEvent()}
// Collect in Composable@Composablefun UsersScreen( viewModel: UsersViewModel, onNavigateToDetail: (Int) -> Unit) { LaunchedEffect(Unit) { viewModel.navigationEvent.collect { event -> when (event) { is NavigationEvent.ToUserDetail -> onNavigateToDetail(event.userId) NavigationEvent.Back -> { /* handle */ } } } }}8. Testing
Phần tiêu đề “8. Testing”@Testfun `loadUsers success updates state`() = runTest { // Given val users = listOf(User(1, "Alice", "alice@email.com")) coEvery { repository.getUsers() } returns Result.success(users)
// When val viewModel = UsersViewModel(repository)
// Then assertEquals(false, viewModel.uiState.value.isLoading) assertEquals(users, viewModel.uiState.value.users)}
@Testfun `loadUsers error updates state`() = runTest { // Given coEvery { repository.getUsers() } returns Result.failure(Exception("Error"))
// When val viewModel = UsersViewModel(repository)
// Then assertEquals("Error", viewModel.uiState.value.error)}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Layer | Responsibility |
|---|---|
| Model | Data và business logic |
| View | UI, observe state |
| ViewModel | Hold state, handle events |
Benefits
Phần tiêu đề “Benefits”- Separation of concerns
- Testable
- Lifecycle-aware
- Configuration change survival