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
| Layer | Responsibility |
|---|---|
| Model | Data và business logic |
| View | UI, observe state |
| ViewModel | Hold state, handle events |
Benefits
- Separation of concerns
- Testable
- Lifecycle-aware
- Configuration change survival
Last updated on