Skip to Content

MVVM Pattern trong Android

1. Giới thiệu

MVVM (Model-View-ViewModel) là architecture pattern được Google khuyến nghị cho Android apps.

2. Components

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ View │────▶│ ViewModel │────▶│ Model │ │ (Composable)│◀────│ │◀────│ (Repository)│ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ Observes Holds State Data Source State Handles Logic (API, DB)

3. Model Layer

// Data class data class User( val id: Int, val name: String, val email: String ) // Repository class 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

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)

@Composable fun UsersScreen( viewModel: UsersViewModel = viewModel() ) { val uiState by viewModel.uiState.collectAsState() UsersContent( state = uiState, onRefresh = viewModel::refresh ) } @Composable fun 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

User Action → ViewModel → Repository → Data Source UI State Recompose UI
// Events từ UI sealed 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) } } } // Usage UsersScreen( onEvent = viewModel::onEvent )

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 @Composable fun 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

@Test fun `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) } @Test fun `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

LayerResponsibility
ModelData và business logic
ViewUI, observe state
ViewModelHold state, handle events

Benefits

  • Separation of concerns
  • Testable
  • Lifecycle-aware
  • Configuration change survival
Last updated on