StateFlow — bản chất, StateFlow vs LiveData trong Android

Tác giả: IT Sectr Đã đăng: 2026-02-20 Thời gian đọc: 9 phút

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

  • StateFlow — một trình giữ trạng thái từ kotlinx.coroutines.flow, luôn lưu trữ một giá trị hiện tại và phát ra khi đăng ký.
  • MutableStateFlow — một StateFlow có thể thay đổi với thuộc tính value có thể thay đổi, được sử dụng bên trong ViewModel và được công bố dưới dạng StateFlow.
  • collect() — một toán tử đầu cuối của Flow để đăng ký thay đổi; cho UI sử dụng collectAsState() trong Compose hoặc repeatOnLifecycle() trong View.
  • StateFlow vs LiveData: StateFlow không phụ thuộc vào Lifecycle, yêu cầu quản lý đăng ký rõ ràng, nhưng hỗ trợ coroutines và đa nền tảng.
  • stateIn() — một toán tử chuyển đổi bất kỳ Flow nào thành StateFlow với chiến lược SharingStarted có thể cấu hình.

StateFlow trong Kotlin là gì?

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.

StateFlow vs LiveData: khác biệt 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íStateFlowLiveData
Nền tảngKotlin Multiplatform (Android, iOS, máy chủ)Chỉ Android
Lifecycle-awareKhông — yêu cầu repeatOnLifecycle()Có — ràng buộc tích hợp
CoroutinesHỗ trợ đầy đủ (map, filter, combine)Qua builder liveData { }
An toàn nullCó — có thể serialize qua kotlinx.serializationCó — qua LiveData<String?> có thể null
Hợp nhất (Conflation)Được hợp nhất — bỏ qua các giá trị trung gianChỉ qua postValue()
Kiểm thửrunTest + Turbine hoặc toán tử tích hợpInstantTaskExecutorRule + 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: công bố và đăng ký

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.

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

StateFlow trong ViewModel: phương pháp hay nhất

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

kotlin
// 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() và SharingStarted: ba chiến lượ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.

kotlin
// 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.

Ví dụ mã: StateFlow trong Kotlin

Ví dụ 1: ViewModel với StateFlow và Compose

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.

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

Ví dụ 2: StateFlow với Room và combine

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.

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

Sự hợp nhất (conflation) trong StateFlow là gì?

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.

Làm thế nào để chuyển đổi LiveData thành StateFlow?

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.

Tại sao StateFlow yêu cầu giá trị ban đầu?

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.

StateFlow có an toàn luồng không?

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 { ... } } }.

Một ViewModel có thể chứa bao nhiêu StateFlow?

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

  • StateFlow — một container phản ứng nóng từ Kotlin Coroutines (replay=1), luôn giữ giá trị cuối cùng.
  • StateFlow vs LiveData: StateFlow độc lập với Lifecycle, hỗ trợ coroutines và đa nền tảng; LiveData có đăng ký tự động.
  • MutableStateFlow với private set và công bố StateFlow chỉ đọc — mẫu tiêu chuẩn cho ViewModel.
  • Sealed class như UIState — cách tiếp cận UDF được Google khuyến nghị để quản lý các trạng thái màn hình phức tạp.
  • stateIn() với WhileSubscribed(5000) — chiến lược tối ưu để chuyển đổi Flow lạnh thành StateFlow cho ViewModel.
  • Room trả về Flow từ DAO — StateFlow + combine + Room thay thế Room + LiveData.
  • Google khuyến nghị StateFlow cho các dự án Kotlin mới, đặc biệt là kết hợp với Jetpack Compose.

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.

Thảo luận dự án

Đọc thêm