State Hoisting là một mẫu trong Jetpack Compose, trong đó trạng thái được chuyển từ hàm Composable con lên hàm cha, và hàm con nhận dữ liệu qua tham số và thông báo thay đổi qua callback. Đây là triển khai của nguyên lý luồng dữ liệu một chiều (UDF), trong đó trạng thái được nâng lên trên và sự kiện được truyền xuống dưới. Theo Google Android Developers, 2026, State Hoisting làm cho các thành phần có thể tái sử dụng, kiểm thử được và dễ dự đoán.
Điểm chính
State Hoisting là một mẫu trong đó hàm Composable không sở hữu trạng thái mà nhận nó từ bên ngoài. Thay vì var bên trong hàm, hai tham số được sử dụng: một giá trị để hiển thị và một callback lambda để xử lý thay đổi. Về mặt kỹ thuật, điều này có nghĩa là thành phần con trở nên không trạng thái (stateless), trong khi cha có trạng thái (stateful).
Ví dụ: thành phần TextField từ Material3 không lưu trữ văn bản đã nhập nội bộ. Nó chấp nhận value: String và onValueChange: (String) -> Unit. Cha gọi TextField khai báo var value by remember { mutableStateOf("") } và truyền value cùng onValueChange. Đây là State Hoisting cổ điển: TextField là thành phần đơn giản (chỉ hiển thị và báo cáo đầu vào), cha là thông minh (sở hữu trạng thái).
Stateless so với Stateful: Thành phần stateless dễ kiểm thử hơn — nó không phụ thuộc vào trạng thái nội bộ, hành vi của nó hoàn toàn được xác định bởi tham số đầu vào. Thành phần stateful thuận tiện cho tạo mẫu nhanh nhưng khó tái sử dụng hơn: nó bị ràng buộc chặt chẽ với một nguồn dữ liệu duy nhất. State Hoisting cho bạn lựa chọn: bất kỳ thành phần nào cũng có thể trở nên stateless bằng cách chuyển trạng thái lên trên.
UDF (Luồng dữ liệu một chiều) là một nguyên lý kiến trúc trong đó dữ liệu di chuyển theo một hướng: từ nguồn sự thật (ViewModel hoặc Composable cha) đến UI, và sự kiện chảy theo hướng ngược lại. State Hoisting là triển khai của UDF ở cấp độ thành phần riêng lẻ. Thay vì mỗi thành phần tự quyết định khi nào và cách thay đổi trạng thái của riêng mình, nó thông báo cho cha về một sự kiện, và cha quyết định cách thay đổi trạng thái.
Lợi ích của UDF: khả năng dự đoán — trạng thái chỉ thay đổi tại một nơi, loại bỏ điều kiện tranh chấp; khả năng truy vết — ngăn xếp cuộc gọi cho phép tái tạo chuỗi thay đổi; kiểm thử — logic có trạng thái có thể được trích xuất vào một lớp riêng và kiểm thử mà không cần UI. Trong các dự án lớn, UDF kết hợp với State Hoisting là tiêu chuẩn thực tế.
Nguồn sự thật duy nhất (Single Source of Truth) là một nguyên lý khác đi kèm với UDF. Mỗi phần trạng thái có chính xác một nguồn. Nếu hai thành phần sử dụng cùng một trạng thái, nguồn phải được chia sẻ (ở cấp ViewModel hoặc cha chung). State Hoisting đảm bảo nguồn nằm ở trên trong hệ thống phân cấp và không xảy ra trùng lặp trạng thái.
| Hướng | Nội dung được truyền | Cách triển khai |
|---|---|---|
| Xuống (cha → con) | Giá trị để hiển thị | Tham số value: T |
| Lên (con → cha) | Sự kiện thay đổi | Tham số onValueChange: (T) -> Unit |
Quy tắc chính: trạng thái nên được nâng lên mức tối thiểu đủ cho tất cả các thành phần cần nó. Nếu trạng thái chỉ được sử dụng trong một thành phần — hãy giữ nó cục bộ. Nếu hai thành phần liền kề cần cùng một trạng thái — hãy nâng lên cha chung. Nếu trạng thái cần trên toàn bộ màn hình — hãy nâng lên ViewModel.
Quy tắc nâng tối thiểu ngăn chặn sự phức tạp không cần thiết. Không có ích gì khi nâng trạng thái trường văn bản lên ViewModel nếu nó chỉ được sử dụng trong một màn hình và không được lưu khi Activity được tạo lại. Sử dụng rememberSaveable ở cấp cha của màn hình, không phải ViewModel, cho trạng thái UI cần tồn tại sau khi xoay màn hình nhưng không cần cho logic nghiệp vụ.
Khi nào nâng lên ViewModel: nếu trạng thái phải tồn tại sau khi Activity được tạo lại, nếu cần cho nhiều màn hình, nếu thay đổi trạng thái kích hoạt logic nghiệp vụ (yêu cầu mạng, cơ sở dữ liệu). State Hoisting ở cấp ViewModel là một mẫu tiêu chuẩn trong kiến trúc MVVM, nơi lớp UI là stateless và ViewModel là stateful.
// ❌ Xấu: thành phần sở hữu trạng thái riêng
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ Tốt: State Hoisting — trạng thái ở cha
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// Cách dùng: cha sở hữu trạng thái
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
Xem xét màn hình đăng nhập với hai trường (email, mật khẩu) và một nút. Cả ba thành phần đều nhận trạng thái qua State Hoisting: email và mật khẩu được quản lý bởi cha, nút nhận trạng thái enabled như một giá trị.
// State Hoisting ở cấp màn hình
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// Trường Email — State Hoisting qua lambda
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// Trường Mật khẩu — tương tự
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// Nút — chỉ nhận enabled (chỉ đọc)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// Thành phần Stateless: nhận email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
singleLine = true
)
}
// Thành phần nút Stateless
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("Đăng nhập")
}
}
EmailField và PasswordField hoàn toàn không trạng thái. Chúng có thể được tái sử dụng trên bất kỳ màn hình nào bằng cách kết nối với bất kỳ nguồn dữ liệu nào. LoginButton nhận enabled dưới dạng chỉ đọc — đây là một dạng khác của State Hoisting nơi trạng thái không được nâng (nút không thể tự kích hoạt) mà được truyền xuống đã sẵn sàng. Cách tiếp cận này mang lại sự linh hoạt tối đa với sự kết hợp tối thiểu giữa các thành phần.
Không phải mọi trạng thái đều cần được nâng. Trạng thái cục bộ (State bên trong Composable) được biện minh khi: dữ liệu chỉ cần trong một thành phần, không ảnh hưởng đến các phần tử lân cận và không cần tồn tại sau khi tái tạo một phần cụ thể. Ví dụ, trạng thái hoạt ảnh, tiêu điểm trường nhập, vị trí cuộn hiện tại — giữ chúng cục bộ là hợp lý.
Khi nào State Hoisting cần thiết: trạng thái được sử dụng bởi nhiều thành phần con; thay đổi ở một con phải được phản ánh ở con khác; logic thay đổi trạng thái cần được kiểm thử riêng biệt với UI; trạng thái phải tồn tại sau khi Activity được tạo lại. Trong những trường hợp này, trạng thái cục bộ tạo ra sự trùng lặp và không nhất quán dữ liệu.
Cách tiếp cận kết hợp: giữ trạng thái tối thiểu cục bộ, nâng phần còn lại. Quy tắc của Compose: “nâng trạng thái cao như cần thiết và thấp như có thể.” Trong thực tế, điều này có nghĩa là bắt đầu với remember cục bộ và chỉ nâng cấp khi cần truy cập từ thành phần khác. Đừng áp dụng State Hoisting một cách phòng ngừa — nó làm phức tạp mã mà không cần thiết.
Câu hỏi thường gặp
State Hoisting là một mẫu ở cấp độ thành phần UI. ViewModel là một lớp kiến trúc cho logic nghiệp vụ. State Hoisting có thể nâng trạng thái lên cấp Composable cha, cấp màn hình hoặc ViewModel. ViewModel là điểm nâng cao nhất cho trạng thái cần tồn tại sau khi Activity được tạo lại.
Thành phần stateless được kiểm thử bằng cách đơn giản truyền giá trị. Gọi Composable với các tham số cần thiết và kiểm tra hiển thị qua ComposeTestRule. Thay đổi trạng thái được kiểm thử ở cấp cha hoặc ViewModel — tách biệt khỏi UI. Điều này đơn giản hóa đáng kể việc kiểm thử: không cần mô phỏng tái tạo bên trong thành phần.
Có, đây là thực hành phổ biến. Nếu thành phần chỉ cần hiển thị dữ liệu mà không có khả năng sửa đổi — hãy truyền State<T> (chỉ đọc). Thành phần sẽ đăng ký thay đổi nhưng không thể khởi tạo chúng. Điều này tăng cường đóng gói và bảo vệ dữ liệu khỏi các biến đổi không mong muốn.
Để truyền sâu, hãy sử dụng CompositionLocal hoặc truyền qua tham số của Composable cha. Nếu trạng thái cần trên toàn bộ màn hình — hãy trích xuất vào ViewModel và sử dụng collectAsState(). Truyền qua 5+ cấp là dấu hiệu của kiến trúc không đúng; hãy xem xét lại hệ thống phân cấp thành phần.
State Hoisting có thể tăng nhẹ số lần tái tạo vì thay đổi ở cha có thể tái tạo tất cả các con. Sử dụng derivedStateOf để lọc thay đổi và keys trong LazyColumn để cập nhật có mục tiêu. Trong hầu hết các kịch bản, chi phí của State Hoisting là không đáng kể so với lợi ích về khả năng bảo trì.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm