State Management
Quản lý state (trạng thái) là một trong những khái niệm quan trọng nhất khi làm việc với Jetpack Compose. Bài này sẽ giải thích từ cơ bản đến nâng cao.
1. State là gì?
Phần tiêu đề “1. State là gì?”State là bất kỳ giá trị nào có thể thay đổi theo thời gian và ảnh hưởng đến giao diện người dùng.
Ví dụ về State:
Phần tiêu đề “Ví dụ về State:”- Số đếm trong Counter app
- Nội dung text trong TextField
- Item được chọn trong danh sách
- Trạng thái loading/error/success
- Theme sáng/tối
Vấn đề: Compose là Declarative
Phần tiêu đề “Vấn đề: Compose là Declarative”@Composablefun Greeting() { var name = "World" // ❌ Không hoạt động!
Button(onClick = { name = "Android" }) { Text("Hello, $name") }}Tại sao không hoạt động?
- Khi bạn click button,
namethay đổi - Nhưng Compose không biết là có thay đổi
- UI không được cập nhật
Giải pháp: mutableStateOf + remember
Phần tiêu đề “Giải pháp: mutableStateOf + remember”@Composablefun Greeting() { var name by remember { mutableStateOf("World") } // ✅ Hoạt động!
Button(onClick = { name = "Android" }) { Text("Hello, $name") // Cập nhật khi name thay đổi }}Giải thích:
mutableStateOf("World")- Tạo state với giá trị ban đầu “World”remember { }- Giữ lại giá trị qua các lần recomposition- Khi
namethay đổi → Compose tự động recompose (vẽ lại UI)
2. remember và mutableStateOf
Phần tiêu đề “2. remember và mutableStateOf”Tại sao cần remember?
Phần tiêu đề “Tại sao cần remember?”Mỗi khi state thay đổi, composable function được gọi lại (recomposition). Nếu không có remember, state sẽ bị reset về giá trị ban đầu.
@Composablefun Counter() { // ❌ SAI: count luôn là 0 vì bị reset mỗi lần recompose var count = mutableStateOf(0)
Button(onClick = { count.value++ }) { Text("Count: ${count.value}") }}
@Composablefun Counter() { // ✅ ĐÚNG: count được giữ lại qua các lần recompose var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Count: $count") }}3 cách khai báo State
Phần tiêu đề “3 cách khai báo State”// Cách 1: Delegation với 'by' (Khuyến nghị - code gọn nhất)var count by remember { mutableStateOf(0) }count++ // Truy cập trực tiếp như biến thường
// Cách 2: Destructuringval (count, setCount) = remember { mutableStateOf(0) }setCount(count + 1) // Dùng function để update
// Cách 3: Trực tiếp (ít dùng)val countState = remember { mutableStateOf(0) }countState.value++ // Phải dùng .valueBest Practice: Dùng cách 1 với
byđể code ngắn gọn nhất.
3. rememberSaveable - Giữ state khi xoay màn hình
Phần tiêu đề “3. rememberSaveable - Giữ state khi xoay màn hình”Khi user xoay màn hình, Activity bị destroy và tạo lại → state bị mất!
// ❌ Mất state khi xoay màn hìnhvar count by remember { mutableStateOf(0) }
// ✅ Giữ state khi xoay màn hìnhvar count by rememberSaveable { mutableStateOf(0) }Khi nào dùng rememberSaveable?
- Input của user (text đã nhập)
- Trạng thái scroll, tab đang chọn
- Data quan trọng không muốn mất khi xoay màn hình
Lưu ý: Chỉ lưu được kiểu dữ liệu đơn giản (Int, String, Boolean…). Với object phức tạp, cần custom Saver hoặc dùng ViewModel.
4. State Hoisting - Nâng state lên component cha
Phần tiêu đề “4. State Hoisting - Nâng state lên component cha”Vấn đề: Stateful Composable khó test và tái sử dụng
Phần tiêu đề “Vấn đề: Stateful Composable khó test và tái sử dụng”// ❌ Stateful: State nằm bên trong@Composablefun Counter() { var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("Count: $count") }}Khó khăn:
- Không thể kiểm soát giá trị từ bên ngoài
- Khó viết test
- Không thể sync với màn hình khác
Giải pháp: State Hoisting
Phần tiêu đề “Giải pháp: State Hoisting”“Hoisting” = nâng lên. Đưa state lên component cha.
// ✅ Stateless: Nhận state từ bên ngoài@Composablefun Counter( count: Int, // State đầu vào onIncrement: () -> Unit, // Event callback modifier: Modifier = Modifier) { Button(onClick = onIncrement, modifier = modifier) { Text("Count: $count") }}
// Stateful container: Quản lý state@Composablefun CounterScreen() { var count by remember { mutableStateOf(0) }
Counter( count = count, onIncrement = { count++ } )}Lợi ích của State Hoisting:
- Tái sử dụng: Dùng Counter ở nhiều nơi với state khác nhau
- Dễ test: Truyền giá trị cố định, kiểm tra output
- Single source of truth: State chỉ ở một chỗ, không bị trùng lặp
Pattern chung: State down, Events up
Phần tiêu đề “Pattern chung: State down, Events up”Parent (có state) │ ├── truyền state xuống ──▶ Child (hiển thị) │ └── nhận events lên ◀──── Child (gửi sự kiện)5. derivedStateOf - Tính toán từ state khác
Phần tiêu đề “5. derivedStateOf - Tính toán từ state khác”Khi bạn cần compute một giá trị từ state khác:
@Composablefun ShoppingCart(items: List<Item>) { // ❌ SAI: Tính lại mỗi lần recompose (dù items không đổi) val totalPrice = items.sumOf { it.price }
// ✅ ĐÚNG: Chỉ tính lại khi items thay đổi val totalPrice by remember(items) { derivedStateOf { items.sumOf { it.price } } }
Text("Total: $$totalPrice")}Ví dụ: Filter danh sách
Phần tiêu đề “Ví dụ: Filter danh sách”@Composablefun SearchableList(items: List<String>) { var searchQuery by remember { mutableStateOf("") }
// Chỉ filter lại khi items hoặc searchQuery thay đổi val filteredItems by remember(items, searchQuery) { derivedStateOf { items.filter { it.contains(searchQuery, ignoreCase = true) } } }
Column { TextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Tìm kiếm") } )
LazyColumn { items(filteredItems) { item -> Text(item) } } }}6. ViewModel + State - Cho ứng dụng thực tế
Phần tiêu đề “6. ViewModel + State - Cho ứng dụng thực tế”Trong ứng dụng thực tế, state thường được quản lý bởi ViewModel:
// Data class đại diện cho UI statedata class CounterUiState( val count: Int = 0, val isLoading: Boolean = false)
// ViewModel quản lý stateclass CounterViewModel : ViewModel() { // Private mutable state private val _uiState = MutableStateFlow(CounterUiState())
// Public read-only state val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow()
fun increment() { _uiState.update { currentState -> currentState.copy(count = currentState.count + 1) } }}
// Composable sử dụng ViewModel@Composablefun CounterScreen(viewModel: CounterViewModel = viewModel()) { // Collect StateFlow as Compose state val uiState by viewModel.uiState.collectAsState()
Column { Text("Count: ${uiState.count}") Button(onClick = { viewModel.increment() }) { Text("Increment") } }}Tại sao dùng ViewModel?
- State survive configuration changes (xoay màn hình)
- Tách biệt logic khỏi UI
- Dễ test
- Có thể load data từ network/database
7. UI State Pattern cho ứng dụng lớn
Phần tiêu đề “7. UI State Pattern cho ứng dụng lớn”// Sealed class cho các trạng thái UIsealed class HomeUiState { object Loading : HomeUiState() data class Success(val items: List<Item>) : HomeUiState() data class Error(val message: String) : HomeUiState()}
// Hoặc dùng data class với các fielddata class HomeUiState( val isLoading: Boolean = false, val items: List<Item> = emptyList(), val error: String? = null)Ví dụ hoàn chỉnh:
Phần tiêu đề “Ví dụ hoàn chỉnh:”class HomeViewModel(private val repository: ItemRepository) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init { loadItems() }
fun loadItems() { viewModelScope.launch { // Bắt đầu loading _uiState.update { it.copy(isLoading = true, error = null) }
try { val items = repository.getItems() // Thành công _uiState.update { it.copy(isLoading = false, items = items) } } catch (e: Exception) { // Lỗi _uiState.update { it.copy(isLoading = false, error = e.message) } } } }}
@Composablefun HomeScreen(viewModel: HomeViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState()
Box(modifier = Modifier.fillMaxSize()) { when { uiState.isLoading -> { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } uiState.error != null -> { Column(modifier = Modifier.align(Alignment.Center)) { Text("Lỗi: ${uiState.error}") Button(onClick = { viewModel.loadItems() }) { Text("Thử lại") } } } else -> { LazyColumn { items(uiState.items) { item -> ItemRow(item) } } } } }}8. Unidirectional Data Flow (UDF)
Phần tiêu đề “8. Unidirectional Data Flow (UDF)”┌─────────────────────────────────────────┐│ UI ││ ││ State ─────────────▶ Hiển thị ││ ▲ │ ││ │ ▼ ││ ViewModel ◀─────────── User Events ││ │└─────────────────────────────────────────┘
State flows DOWN (từ ViewModel xuống UI)Events flow UP (từ UI lên ViewModel)Lợi ích của UDF:
- Dễ debug: State chỉ có một nguồn
- Dễ test: Mock ViewModel, kiểm tra UI
- Consistent: Không có state trùng lặp
📝 Tóm tắt cho người mới
Phần tiêu đề “📝 Tóm tắt cho người mới”| Concept | Khi nào dùng | Ví dụ |
|---|---|---|
remember |
Giữ state qua recomposition | Counter trong 1 màn hình |
rememberSaveable |
Giữ state khi xoay màn hình | Input text, scroll position |
| State hoisting | Tái sử dụng component | Button, TextField tùy chỉnh |
derivedStateOf |
Tính toán từ state khác | Filtered list, total price |
| ViewModel | App thực tế | Fetch data, business logic |
Quy tắc vàng
Phần tiêu đề “Quy tắc vàng”- State ở mức thấp nhất cần thiết
- Hoist state lên khi cần chia sẻ hoặc điều khiển từ bên ngoài
- Dùng ViewModel cho state phức tạp hoặc cần survive config changes
- State down, Events up - luôn ghi nhớ pattern này!