Phân biệt effect, intent và state trong mô hình MVI
Trong mô hình MVI (Model-View-Intent), khi kết hợp với Jetpack Compose, các thành phần Effect, Intent, và State đóng vai trò quan trọng để quản lý luồng dữ liệu và tương tác giữa UI và logic. Dưới đây là sự phân biệt giữa chúng qua định nghĩa và ví dụ.
1. Intent (Ý định của người dùng)
Intent đại diện cho ý định của người dùng hoặc sự kiện từ UI gửi đến ViewModel. Nó cho biết người dùng muốn làm gì.
- Nguồn: Phát sinh từ UI (thường là hành động của người dùng như bấm nút, cuộn danh sách, nhập văn bản...).
- Mục tiêu: Thông báo cho ViewModel để xử lý và đưa ra thay đổi (cập nhật
State
hoặc phát raEffect
). - Ví dụ: Bấm nút tải dữ liệu, kéo để làm mới, hoặc chọn một mục.
Ví dụ mã:
sealed class UserIntent {
object LoadData : UserIntent()
data class SelectItem(val id: Int) : UserIntent()
object Refresh : UserIntent()
}
Trong Jetpack Compose:
Button(onClick = { onIntent(UserIntent.LoadData) }) {
Text("Load Data")
}
2. State (Trạng thái của UI)
State đại diện cho trạng thái hiện tại của giao diện người dùng. Nó chứa tất cả dữ liệu cần thiết để hiển thị giao diện ở thời điểm hiện tại.
- Nguồn: Được ViewModel tính toán và cập nhật.
- Mục tiêu: Để UI phản ánh chính xác trạng thái hiện tại.
- Quy tắc: State luôn phải immutable (không thay đổi trực tiếp), và Compose sẽ tự động recompose khi
State
thay đổi.
Ví dụ mã:
data class ViewState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String? = null
)
Trong Jetpack Compose:
if (state.isLoading) {
CircularProgressIndicator()
} else {
LazyColumn {
items(state.items) { item ->
Text(item)
}
}
}
3. Effect (Sự kiện một lần hoặc hành động phụ)
Effect là sự kiện một lần hoặc tác động phụ cần được thực thi ngoài phạm vi quản lý của Compose. Thường là các hành động không liên quan trực tiếp đến UI, hoặc không nên tái lặp (idempotent).
- Nguồn: Phát sinh từ ViewModel khi xử lý
Intent
hoặc thay đổiState
. - Mục tiêu: Kích hoạt hành động bên ngoài như hiển thị thông báo, chuyển màn hình, hoặc gọi API.
- Quy tắc: Không để lại trạng thái UI liên tục, thường là sự kiện một lần.
Ví dụ mã:
sealed class UiEffect {
object ShowToast : UiEffect()
data class NavigateToDetail(val id: Int) : UiEffect()
}
Trong Jetpack Compose:
LaunchedEffect(effect) {
when (effect) {
UiEffect.ShowToast -> Toast.makeText(context, "Action completed", Toast.LENGTH_SHORT).show()
is UiEffect.NavigateToDetail -> navController.navigate("detail/${effect.id}")
}
}
Mối quan hệ giữa Intent, State, và Effect
- User Intent → ViewModel: Người dùng gửi
Intent
để yêu cầu thực hiện hành động. - ViewModel → State: Xử lý
Intent
, cập nhậtState
để UI phản ánh trạng thái mới. - ViewModel → Effect: Phát ra
Effect
để thực hiện các hành động một lần (như điều hướng hoặc thông báo).
Ví dụ tổng hợp
// Intent
sealed class UserIntent {
object LoadData : UserIntent()
object Refresh : UserIntent()
data class SelectItem(val id: Int) : UserIntent()
}
// State
data class ViewState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String? = null
)
// Effect
sealed class UiEffect {
object ShowErrorToast : UiEffect()
data class NavigateToDetail(val id: Int) : UiEffect()
}
// ViewModel xử lý
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow(ViewState())
val state: StateFlow<ViewState> = _state.asStateFlow()
private val _effect = Channel<UiEffect>()
val effect = _effect.receiveAsFlow()
fun processIntent(intent: UserIntent) {
when (intent) {
UserIntent.LoadData -> loadData()
UserIntent.Refresh -> refreshData()
is UserIntent.SelectItem -> navigateToDetail(intent.id)
}
}
private fun loadData() { /* Cập nhật state */ }
private fun refreshData() { /* Xử lý refresh */ }
private fun navigateToDetail(id: Int) {
viewModelScope.launch {
_effect.send(UiEffect.NavigateToDetail(id))
}
}
}
// UI sử dụng
@Composable
fun MyScreen(viewModel: MyViewModel) {
val state by viewModel.state.collectAsState()
val effect = viewModel.effect.collectAsEffect()
// Render UI dựa trên State
if (state.isLoading) {
CircularProgressIndicator()
} else {
LazyColumn {
items(state.items) { item ->
Text(item)
}
}
}
// Xử lý Effect
LaunchedEffect(effect) {
when (effect) {
UiEffect.ShowErrorToast -> Toast.makeText(context, "Error occurred", Toast.LENGTH_SHORT).show()
is UiEffect.NavigateToDetail -> navController.navigate("detail/${effect.id}")
}
}
}
Tóm tắt
Thành phần | Vai trò | Nguồn gốc | Mục tiêu |
---|---|---|---|
Intent | Yêu cầu hoặc ý định từ người dùng | Giao diện (UI) | Gửi tới ViewModel để xử lý |
State | Trạng thái hiện tại của UI | ViewModel | Render UI dựa trên trạng thái |
Effect | Sự kiện một lần hoặc hành động bên ngoài (navigation, toast...) | ViewModel hoặc xử lý Intent | Kích hoạt hành động ngoài UI |
Nhận xét
Đăng nhận xét