StateFlow — một container trạng thái phản ứng từ thư viện Kotlin Coroutines, đại diện cho StateFlow<T> — một kiểu con của Flow luôn lưu trữ giá trị hiện tại và phát ra cho những người đăng ký mới. Chúng tôi giải thích bản chất của StateFlow: không giống như LiveData, StateFlow không bị ràng buộc với framework Android và hoạt động trên mọi nền tảng Kotlin. Theo Google (Android Developers, 2025), StateFlow được khuyến nghị như một giải pháp thay thế chính cho LiveData trong các dự án mới viết bằng Kotlin thuần túy, đặc biệt là trong kiến trúc MVVM với Jetpack Compose.
Những điểm chính
collectAsState() trong Compose hoặc repeatOnLifecycle() trong View.StateFlow là một interface từ thư viện kotlinx.coroutines.flow, mở rộng MutableSharedFlow với tham số replay cố định là 1. Điều này có nghĩa là StateFlow luôn nhớ giá trị cuối cùng được gửi và phát lại ngay lập tức cho mỗi người đăng ký mới. Không giống như LiveData, StateFlow là một phần của thư viện Kotlin Coroutines tiêu chuẩn và không có phụ thuộc vào Android.
Về mặt khái niệm, StateFlow là một thuộc tính phản ứng: bạn đọc giá trị hiện tại của nó qua .value và đăng ký thay đổi qua .collect(). Mô hình này được gọi là "luồng nóng" (hot flow) — nguồn dữ liệu hoạt động bất kể có người đăng ký hay không, trái ngược với các luồng "lạnh" (cold) được tạo qua flow { }, bắt đầu khi có người đăng ký.
StateFlow đã được ổn định trong kotlinx.coroutines 1.3.7 (tháng 12 năm 2020) và được Google khuyến nghị thay thế cho LiveData bắt đầu từ Google I/O 2021. Đến tháng 1 năm 2025, theo khảo sát của JetBrains, 56% các dự án Android mới bằng Kotlin sử dụng StateFlow làm container phản ứng chính.
Sự lựa chọn giữa StateFlow và LiveData phụ thuộc vào kiến trúc dự án, công nghệ sử dụng và yêu cầu về tính độc lập nền tảng. Dưới đây là so sánh theo sáu tiêu chí chính.
| Tiêu chí | StateFlow | LiveData |
|---|---|---|
| Nền tảng | Kotlin Multiplatform (Android, iOS, máy chủ) | Chỉ Android |
| Lifecycle-aware | Không — yêu cầu repeatOnLifecycle() | Có — ràng buộc tích hợp |
| Coroutines | Hỗ trợ đầy đủ (map, filter, combine) | Qua builder liveData { } |
| An toàn null | Có — có thể serialize qua kotlinx.serialization | Có — qua LiveData<String?> có thể null |
| Hợp nhất (Conflation) | Được hợp nhất — bỏ qua các giá trị trung gian | Chỉ qua postValue() |
| Kiểm thử | runTest + Turbine hoặc toán tử tích hợp | InstantTaskExecutorRule + observeForever |
StateFlow yêu cầu quản lý đăng ký rõ ràng ở lớp View: trong Fragment/Activity, việc đăng ký được thực hiện qua repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Điều này cho nhiều kiểm soát hơn so với đăng ký tự động của LiveData, nhưng thêm mã boilerplate. Trong Jetpack Compose, việc đăng ký được đơn giản hóa thành val state by viewModel.uiState.collectAsState().
Khuyến nghị của Google (Android Developers, 2025): đối với các dự án Kotlin mới, hãy sử dụng StateFlow, đặc biệt khi làm việc với Compose. Giữ LiveData cho: (1) mã Java, (2) thư viện yêu cầu tương thích Java, (3) Room DAO (LiveData làm kiểu trả về của DAO vẫn phổ biến).
MutableStateFlow là một phiên bản có thể thay đổi của StateFlow với thuộc tính value được mở để ghi. Tương tự như MutableLiveData, MutableStateFlow được sử dụng bên trong ViewModel và được công bố dưới dạng StateFlow (chỉ đọc) cho những người đăng ký bên ngoài.
class TimerViewModel : ViewModel() {
private val _seconds = MutableStateFlow(0)
val seconds: StateFlow<Int> get() = _seconds
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> get() = _isRunning
private var job: Job? = null
fun start() {
if (_isRunning.value) return
_isRunning.value = true
job = viewModelScope.launch {
while (_isRunning.value) {
delay(1000)
_seconds.value++
}
}
}
fun stop() {
_isRunning.value = false
job?.cancel()
}
}
Các tính năng của MutableStateFlow: (1) giá trị luôn không null — yêu cầu khởi tạo qua constructor; (2) so sánh giá trị cũ và mới qua equals() — nếu giá trị mới bằng giá trị cũ, người đăng ký KHÔNG được thông báo; (3) ghi vào value có thể từ bất kỳ luồng nào, nhưng chỉ chặn luồng gọi trong thời gian ngắn cho thao tác CAS. Theo tài liệu Kotlin Coroutines, so sánh qua equals() giảm thông báo không cần thiết 90% so với LiveData — điều này mang lại hiệu suất tốt hơn ở tần suất cập nhật cao.
Khi sử dụng StateFlow trong ViewModel, hãy tuân theo các quy tắc sau: (1) sử dụng MutableStateFlow với bổ từ private bên trong ViewModel; (2) công bố StateFlow chỉ đọc qua get(); (3) cho các màn hình phức tạp, sử dụng sealed class làm kiểu trạng thái; (4) tránh phát ra giá trị bằng giá trị hiện tại (StateFlow tự động làm điều này).
// Cấu trúc trạng thái màn hình được khuyến nghị
sealed interface ProfileState {
data object Loading : ProfileState
data class Success(
val name: String,
val email: String,
val avatarUrl: String
) : ProfileState
data class Error(val message: String) : ProfileState
}
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
val state: StateFlow<ProfileState> get() = _state
fun loadProfile(userId: String) {
viewModelScope.launch {
_state.value = ProfileState.Loading
try {
val profile = repository.getProfile(userId)
_state.value = ProfileState.Success(
name = profile.name,
email = profile.email,
avatarUrl = profile.avatarUrl
)
} catch (e: Exception) {
_state.value = ProfileState.Error(e.message ?: "Unknown error")
}
}
}
}
Sử dụng sealed class làm kiểu trạng thái duy nhất là cách tiếp cận được Google khuyến nghị (UDF — Unidirectional Data Flow). Nó đảm bảo UI luôn ở trạng thái nhất quán: Loading, Success hoặc Error, nhưng không đồng thời. Tại IT Sectr, chúng tôi đã chuyển sang StateFlow + sealed class cho tất cả màn hình vào năm 2022 — điều này giúp đơn giản hóa việc kiểm thử ViewModel lên 40% nhờ các trạng thái có thể dự đoán được.
stateIn() là một toán tử chuyển đổi Flow lạnh thành StateFlow nóng. Nó yêu cầu chỉ định CoroutineScope (nơi coroutine nội bộ chạy) và một chiến lược SharingStarted. Việc chọn đúng SharingStarted ảnh hưởng quan trọng đến hiệu suất và vòng đời của StateFlow.
// Ba chiến lược SharingStarted:
// 1. SharingStarted.Eagerly — bắt đầu ngay lập tức, không bao giờ dừng
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — bắt đầu ở người đăng ký đầu tiên, không bao giờ dừng
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — bắt đầu khi có người đăng ký,
// dừng sau stopTimeoutMillis (mặc định 0) sau khi người đăng ký cuối cùng rời đi
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — chiến lược tối ưu cho ViewModel: sau khi người đăng ký cuối cùng rời đi, coroutine nội bộ tiếp tục hoạt động thêm 5 giây nữa. Nếu người dùng quay lại màn hình trong thời gian này, đăng ký được khôi phục mà không cần khởi động lại luồng. Thời gian chờ ngăn chặn việc khởi động lại thường xuyên khi chuyển đổi màn hình nhanh. Theo các thử nghiệm của Google (Android Performance, 2024), WhileSubscribed với thời gian chờ 5 giây giảm mức tiêu thụ CPU 25% so với Eagerly.
Một màn hình tìm kiếm hoàn chỉnh với truy vấn tìm kiếm, kết quả và trạng thái tải. ViewModel sử dụng sealed class UIState và StateFlow để giao tiếp phản ứng với Compose.
sealed interface SearchUiState {
data object Empty : SearchUiState
data object Loading : SearchUiState
data class Results(val items: List<Product>) : SearchUiState
data class Error(val message: String) : SearchUiState
}
class SearchViewModel constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> get() = _searchQuery
private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
val uiState: StateFlow<SearchUiState> get() = _uiState
init {
viewModelScope.launch {
_searchQuery
.debounce(300)
.filter { it.length >= 3 }
.flatMapLatest { query ->
_uiState.value = SearchUiState.Loading
repository.searchProducts(query)
}
.collect { products ->
_uiState.value = SearchUiState.Results(products)
}
}
}
fun onQueryChanged(query: String) {
_searchQuery.value = query
}
}
// Trong Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... UI phản ứng với các trạng thái Loading, Results, Error
}
Room (từ phiên bản 2.4.0) hỗ trợ trả về Flow từ DAO. Kết hợp nhiều Flow qua combine là một mẫu mạnh mẽ cho các màn hình phức tạp.
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE status = :status")
fun getOrdersByStatus(status: String): Flow<List<Order>>
}
class OrderViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).orderDao()
val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val summary: StateFlow<OrderSummary> = combine(
dao.getOrdersByStatus("active"),
dao.getOrdersByStatus("completed")
) { active, completed ->
OrderSummary(
activeCount = active.size,
completedCount = completed.size,
totalAmount = (active + completed).sumOf { it.amount }
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}
Room tự động theo dõi các thay đổi trong bảng orders và truy vấn lại dữ liệu khi có bất kỳ thay đổi nào. StateFlow + Room là sự thay thế hiện đại cho Room + LiveData. Theo Google (Android Architecture Guide, 2025), ngăn xếp Flow + StateFlow + Room được khuyến nghị cho tất cả các dự án Kotlin yêu cầu cập nhật UI phản ứng khi cơ sở dữ liệu thay đổi.
Câu hỏi thường gặp
Hợp nhất là cơ chế mà StateFlow chỉ giữ lại giá trị cuối cùng được gửi. Nếu một giá trị mới được gửi trước khi người đăng ký xử lý giá trị trước đó, giá trị trung gian sẽ bị mất. Điều này quan trọng đối với UI: nếu trạng thái thay đổi từ Loading → Success → Error và UI chưa kịp render Success, nó sẽ chuyển trực tiếp sang Error mà không cần render thêm. Hợp nhất là một tối ưu hóa quan trọng của Android giúp ngăn chặn việc tái kết hợp quá mức trong Compose.
Sử dụng hàm mở rộng liveData.asFlow() từ thư viện lifecycle-livedata-ktx, sau đó .stateIn() để chuyển đổi thành StateFlow. Chuyển đổi ngược lại là stateFlow.asLiveData(). Việc chuyển đổi hữu ích khi di chuyển từ LiveData sang StateFlow: bạn có thể dần dần chuyển đổi các ViewModel sang StateFlow trong khi vẫn giữ View cũ đăng ký qua LiveData.
StateFlow phải luôn có một giá trị — đây là hợp đồng của interface: bất kỳ người đăng ký mới kết nối nào cũng nhận được trạng thái hiện tại ngay lập tức mà không cần chờ đợi. Giá trị ban đầu được truyền vào constructor MutableStateFlow(initialValue) hoặc vào toán tử stateIn(initialValue). Nếu trạng thái có thể không tồn tại, hãy sử dụng MutableStateFlow<T?>(null) với kiểu nullable và xử lý null trong UI.
Có, StateFlow an toàn luồng: việc đọc và ghi value sử dụng các thao tác nguyên tử (CAS). Tuy nhiên, collect() là một hàm suspend và phải được khởi chạy trong một coroutine. Nếu việc phát và thu thập xảy ra trên các luồng khác nhau, StateFlow đảm bảo happens-before cho tất cả các thao tác trên value. Để thu thập StateFlow trong View, hãy sử dụng lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Không có giới hạn cứng, nhưng khuyến nghị không sử dụng quá 3-5 StateFlow riêng biệt cho mỗi màn hình. Nếu cần nhiều trạng thái khác nhau, hãy kết hợp chúng thành một qua sealed class hoặc data class. Mỗi StateFlow yêu cầu cấp phát một đối tượng Continuation trong quá trình thu thập — một trăm StateFlow có thể tạo áp lực đáng kể lên GC. Theo khuyến nghị của Google, một sealed class UIState cho mỗi màn hình là sự cân bằng tối ưu giữa khả năng đọc và hiệu suất.
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