Bỏ qua để đến nội dung

MVVM Pattern

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

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:#4caf50
// 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)
}
}
}
}
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()
}
@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)
}
}
}
}
}
}
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ừ 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
)
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 */ }
}
}
}
}
@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)
}

Layer Responsibility
Model Data và business logic
View UI, observe state
ViewModel Hold state, handle events
  • Separation of concerns
  • Testable
  • Lifecycle-aware
  • Configuration change survival