Bỏ qua để đến nội dung

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.

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.

  • 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
@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
@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)

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")
}
}
// 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

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ì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

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
@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

“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
Parent (có state)
├── truyền state xuống ──▶ Child (hiển thị)
└── nhận events lên ◀──── Child (gửi sự kiện)

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")
}
@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ế

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

// 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
)
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)
}
}
}
}
}
}

┌─────────────────────────────────────────┐
│ 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

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
  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!