StateFlow — isang reaktibong lalagyan ng estado mula sa library ng Kotlin Coroutines, na kumakatawan sa StateFlow<T> — isang subtype ng Flow na laging nag-iimbak ng kasalukuyang halaga at naglalabas nito sa mga bagong subscriber. Ipinapaliwanag ang kakanyahan ng StateFlow: hindi tulad ng LiveData, ang StateFlow ay hindi nakatali sa Android framework at gumagana sa anumang platform ng Kotlin. Ayon sa Google (Android Developers, 2025), ang StateFlow ay inirerekomenda bilang pangunahing alternatibo sa LiveData para sa mga bagong proyekto sa purong Kotlin, lalo na sa arkitekturang MVVM na may Jetpack Compose.
Mga Pangunahing Punto
collectAsState() sa Compose o repeatOnLifecycle() sa View.StateFlow ay isang interface mula sa library na kotlinx.coroutines.flow na nagpapalawak ng MutableSharedFlow na may nakapirming parameter na replay = 1. Nangangahulugan ito na laging naaalala ng StateFlow ang huling ipinadalang halaga at agad itong pinapatugtog sa bawat bagong subscriber. Hindi tulad ng LiveData, ang StateFlow ay bahagi ng standard na library ng Kotlin Coroutines at walang mga dependency sa Android.
Sa konsepto, ang StateFlow ay isang reaktibong pag-aari: binabasa mo ang kasalukuyang halaga nito sa pamamagitan ng .value at nag-subscribe sa mga pagbabago sa pamamagitan ng .collect(). Ang modelong ito ay tinatawag na «mainit» na daloy (hot flow) — ang pinagmumulan ng data ay aktibo kahit na walang mga subscriber, hindi katulad ng «malamig» (cold) na mga daloy na nilikha sa pamamagitan ng flow { } na nagsisimula kapag may lumitaw na subscriber.
Ang StateFlow ay na-stabilize sa kotlinx.coroutines 1.3.7 (Disyembre 2020) at inirerekomenda ng Google bilang kapalit ng LiveData simula sa Google I/O 2021. Pagsapit ng Enero 2025, ayon sa survey ng JetBrains, 56% ng mga bagong proyekto sa Android na gumagamit ng Kotlin ay gumagamit ng StateFlow bilang pangunahing reaktibong lalagyan.
Ang pagpili sa pagitan ng StateFlow at LiveData ay nakasalalay sa arkitektura ng proyekto, teknolohikal na stack, at mga kinakailangan para sa pagsasarili sa platform. Sa ibaba — paghahambing batay sa anim na pangunahing pamantayan.
| Pamantayan | StateFlow | LiveData |
|---|---|---|
| Platform | Kotlin Multiplatform (Android, iOS, server) | Android lamang |
| Lifecycle-aware | Hindi — nangangailangan ng repeatOnLifecycle() | Oo — built-in na pagbubuklod |
| Coroutine | Buong suporta (map, filter, combine) | Sa pamamagitan ng liveData { } builder |
| Kaligtasan sa null | Oo — na-serialize sa pamamagitan ng kotlinx.serialization | Oo — sa pamamagitan ng nullability LiveData<String?> |
| Konflasyon | Conflated — nilalaktawan ang mga halagang nasa pagitan | Sa pamamagitan lamang ng postValue() |
| Pagsubok | runTest + Turbine o mga built-in na operator | InstantTaskExecutorRule + observeForever |
Ang StateFlow ay nangangailangan ng tahasang pamamahala ng subscription sa View layer: sa Fragment/Activity ang subscription ay ginagawa sa pamamagitan ng repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Nagbibigay ito ng higit na kontrol kaysa sa awtomatikong subscription ng LiveData, ngunit nagdaragdag ng boilerplate code. Sa Jetpack Compose ang subscription ay pinapasimple sa val state by viewModel.uiState.collectAsState().
Rekomendasyon ng Google (Android Developers, 2025): para sa mga bagong proyekto sa Kotlin gamitin ang StateFlow, lalo na kapag nagtatrabaho sa Compose. Iwanan ang LiveData para sa: (1) Java code, (2) mga library na nangangailangan ng compatibility sa Java, (3) Room DAO (ang LiveData bilang uri ng pagbabalik ng DAO ay popular pa rin).
MutableStateFlow — ang nababagong bersyon ng StateFlow na may bukas na property na value para sa pagsulat. Kahalintulad ng MutableLiveData, ang MutableStateFlow ay ginagamit sa loob ng ViewModel at inilalathala bilang StateFlow (read-only) para sa mga panlabas na subscriber.
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()
}
}
Mga katangian ng MutableStateFlow: (1) ang halaga ay palaging non-null — nangangailangan ng pagsisimula sa pamamagitan ng constructor; (2) paghahambing ng luma at bagong mga halaga sa pamamagitan ng equals() — kung ang bagong halaga ay katumbas ng luma, ang mga subscriber ay HINDI naaabisuhan; (3) ang pagsulat sa value ay posible mula sa anumang thread, ngunit hinaharangan ang tumatawag na thread para lamang sa maikling tagal ng CAS operation. Ayon sa dokumentasyon ng Kotlin Coroutines, ang paghahambing sa pamamagitan ng equals() ay nagbabawas ng bilang ng mga hindi kinakailangang abiso ng 90% kumpara sa LiveData — nagbibigay ito ng pagtaas ng pagganap sa mataas na dalas ng pag-update.
Kapag gumagamit ng StateFlow sa ViewModel sundin ang mga sumusunod na patakaran: (1) gamitin ang MutableStateFlow na may private modifier sa loob ng ViewModel; (2) ilathala ang read-only na StateFlow sa pamamagitan ng get(); (3) para sa mga kumplikadong screen gamitin ang sealed class bilang estado; (4) iwasan ang paglalabas ng halagang katumbas ng kasalukuyan (awtomatikong ginagawa ito ng StateFlow).
// Inirerekomendang istraktura ng estado ng screen
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")
}
}
}
}
Ang paggamit ng sealed class bilang nag-iisang uri ng estado — ang inirerekomendang diskarte ng Google (UDF — Unidirectional Data Flow). Ginagarantiya nito na ang UI ay laging nasa pare-parehong estado: Loading, Success o Error, ngunit hindi sabay-sabay. Sa IT Sectr lumipat kami sa StateFlow + sealed class para sa lahat ng screen noong 2022 — pinasimple nito ang pagsubok ng ViewModel ng 40% dahil sa mga mahuhulaang estado.
stateIn() — operator na nagko-convert ng malamig na Flow sa mainit na StateFlow. Nangangailangan ng pagtukoy ng CoroutineScope (kung saan sinisimulan ang panloob na coroutine) at ng SharingStarted na diskarte. Ang tamang pagpili ng SharingStarted ay kritikal na nakakaapekto sa pagganap at siklo ng buhay ng StateFlow.
// Tatlong diskarte ng SharingStarted:
// 1. SharingStarted.Eagerly — magsisimula kaagad, hindi humihinto
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — magsisimula sa unang subscriber, hindi humihinto
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — magsisimula kapag may mga subscriber,
// humihinto pagkatapos ng stopTimeoutMillis (default 0) pagkaalis ng huli
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — ang pinakamainam na diskarte para sa ViewModel: pagkatapos umalis ng huling subscriber, ang panloob na coroutine ay patuloy na gumagana nang 5 segundo pa. Kung bumalik ang gumagamit sa screen sa panahong ito, ang subscription ay naibabalik nang hindi nirere-restart ang daloy. Pinipigilan ng timeout ang madalas na pag-restart kapag mabilis na lumilipat sa pagitan ng mga screen. Ayon sa mga pagsubok ng Google (Android Performance, 2024), ang WhileSubscribed na may 5 segundong timeout ay nagbabawas ng konsumo ng CPU ng 25% kumpara sa Eagerly.
Isang kumpletong screen ng paghahanap na may query, mga resulta, at estado ng pag-load. Gumagamit ang ViewModel ng sealed class UIState at StateFlow para sa reaktibong komunikasyon sa 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
}
}
// Sa Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... UI na tumutugon sa mga estado ng Loading, Results, Error
}
Ang Room (mula sa bersyon 2.4.0) ay sumusuporta sa pagbabalik ng Flow mula sa DAO. Ang pagsasama-sama ng maramihang Flow sa pamamagitan ng combine ay isang makapangyarihang pattern para sa mga kumplikadong screen.
@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))
}
Awtomatikong sinusubaybayan ng Room ang mga pagbabago sa talahanayan ng orders at muling kinukuha ang data sa bawat pagbabago. Ang StateFlow + Room — ang modernong kapalit ng pares na Room + LiveData. Ayon sa Google (Android Architecture Guide, 2025), ang kombinasyon ng Flow + StateFlow + Room ay inirerekomenda para sa lahat ng proyekto sa Kotlin na nangangailangan ng reaktibong pag-update ng UI kapag nagbago ang database.
Mga Madalas Itanong
Konflasyon — mekanismo kung saan ang StateFlow ay nag-iimbak lamang ng huling ipinadalang halaga. Kung ang isang bagong halaga ay ipinadala bago naproseso ng subscriber ang nauna, ang halagang nasa pagitan ay nawawala. Ito ay mahalaga para sa UI: kung ang estado ay nagbabago mula Loading → Success → Error, at hindi naabutan ng UI na i-render ang Success, ito ay diretso sa Error nang walang hindi kinakailangang rendering. Ang konflasyon ay ang pangunahing optimisasyon ng Android na pumipigil sa labis na recomposition sa Compose.
Gamitin ang extension function na liveData.asFlow() mula sa library na lifecycle-livedata-ktx, pagkatapos ay .stateIn() para i-convert sa StateFlow. Ang baligtad na conversion — stateFlow.asLiveData(). Ang conversion ay kapaki-pakinabang sa paglipat mula LiveData patungong StateFlow: maaari mong unti-unting ilipat ang ViewModel sa StateFlow, na pinapanatili ang subscription ng lumang View sa pamamagitan ng LiveData.
Ang StateFlow ay dapat laging may halaga — ito ang kontrata ng interface: bawat bagong konektadong subscriber ay agad na tumatanggap ng kasalukuyang estado nang hindi naghihintay. Ang paunang halaga ay ipinapasa sa constructor ng MutableStateFlow(initialValue) o sa operator na stateIn(initialValue). Kung maaaring wala ang estado, gamitin ang MutableStateFlow<T?>(null) na may nullable na uri at hawakan ang null sa UI.
Oo, ang StateFlow ay thread-safe: ang pagsulat at pagbasa ng value ay gumagamit ng mga atomikong operasyon (CAS). Gayunpaman, ang collect() ay isang suspend function at dapat na patakbuhin sa isang coroutine. Kung ang emisyon at collect ay isinasagawa sa magkaibang mga thread, ginagarantiya ng StateFlow ang happens-before para sa lahat ng operasyon sa value. Para sa pagkolekta ng StateFlow sa View, gamitin ang lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Walang limitasyon, ngunit inirerekomenda na hindi hihigit sa 3-5 magkakahiwalay na StateFlow bawat screen. Kung kinakailangan ang mas maraming iba't ibang estado, pagsamahin ang mga ito sa isa sa pamamagitan ng sealed class o data class. Ang bawat StateFlow ay nangangailangan ng paglalaan ng Continuation object kapag nangongolekta — isang daang StateFlow ay maaaring lumikha ng kapansin-pansing pagkarga sa GC. Ayon sa rekomendasyon ng Google, isang sealed class na UIState bawat screen ay ang pinakamainam na balanse sa pagitan ng pagiging madaling mabasa at pagganap.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din