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 ra Effect).
  • 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ụ)

Effectsự 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 đổi State.
  • 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

  1. User Intent → ViewModel: Người dùng gửi Intent để yêu cầu thực hiện hành động.
  2. ViewModel → State: Xử lý Intent, cập nhật State để UI phản ánh trạng thái mới.
  3. 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

Bài đăng phổ biến từ blog này

Tài liệu Clickermann - by Mai Bao Long

Code mẫu tự động phát hiện và lock window screen

Code trái tim