Skip to Content

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, name thay đổ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 name thay đổ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 .value

Best 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:

  1. Tái sử dụng: Dùng Counter ở nhiều nơi với state khác nhau
  2. Dễ test: Truyền giá trị cố định, kiểm tra output
  3. 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

ConceptKhi nào dùngVí dụ
rememberGiữ state qua recompositionCounter trong 1 màn hình
rememberSaveableGiữ state khi xoay màn hìnhInput text, scroll position
State hoistingTái sử dụng componentButton, TextField tùy chỉnh
derivedStateOfTính toán từ state khácFiltered list, total price
ViewModelApp thực tếFetch data, business logic

Quy tắc vàng

  1. State ở mức thấp nhất cần thiết
  2. Hoist state lên khi cần chia sẻ hoặc điều khiển từ bên ngoài
  3. Dùng ViewModel cho state phức tạp hoặc cần survive config changes
  4. State down, Events up - luôn ghi nhớ pattern này!
Last updated on