State Management trong Jetpack Compose
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ì?
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:
- 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
@Composable
fun 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
@Composable
fun 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
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.
@Composable
fun 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}")
}
}
@Composable
fun 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
// 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: Destructuring
val (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
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ình
var count by remember { mutableStateOf(0) }
// ✅ Giữ state khi xoay màn hình
var 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
Vấn đề: Stateful Composable khó test và tái sử dụng
// ❌ Stateful: State nằm bên trong
@Composable
fun 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
“Hoisting” = nâng lên. Đưa state lên component cha.
// ✅ Stateless: Nhận state từ bên ngoài
@Composable
fun 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
@Composable
fun 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
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
Khi bạn cần compute một giá trị từ state khác:
@Composable
fun 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
@Composable
fun 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ế
Trong ứng dụng thực tế, state thường được quản lý bởi ViewModel:
// Data class đại diện cho UI state
data class CounterUiState(
val count: Int = 0,
val isLoading: Boolean = false
)
// ViewModel quản lý state
class 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
@Composable
fun 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
// Sealed class cho các trạng thái UI
sealed 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 field
data class HomeUiState(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null
)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) }
}
}
}
}
@Composable
fun 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)
┌─────────────────────────────────────────┐
│ 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
| 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
- 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!