StateFlow — reaktivní kontejner stavu z knihovny Kotlin Coroutines, představující StateFlow<T> — podtyp Flow, který vždy uchovává aktuální hodnotu a emituje ji novým odběratelům. Vysvětlujeme podstatu StateFlow: na rozdíl od LiveData není StateFlow vázán na Android framework a funguje na jakékoli platformě Kotlin. Podle Google (Android Developers, 2025) je StateFlow doporučen jako hlavní alternativa LiveData pro nové projekty v čistém Kotlin, zejména v architektuře MVVM s Jetpack Compose.
Hlavní body
collectAsState() v Compose nebo repeatOnLifecycle() ve View.StateFlow — je rozhraní z knihovny kotlinx.coroutines.flow, rozšiřující MutableSharedFlow s pevným parametrem replay = 1. To znamená, že StateFlow si vždy pamatuje poslední odeslanou hodnotu a okamžitě ji přehrává každému novému odběrateli. Na rozdíl od LiveData je StateFlow součástí standardní knihovny Kotlin Coroutines a nemá žádné závislosti na Androidu.
Koncepčně je StateFlow reaktivní vlastnost: aktuální hodnotu čtete přes .value a změny odebíráte přes .collect(). Tomuto modelu se říká „horký” tok (hot flow) — zdroj dat je aktivní nezávisle na přítomnosti odběratelů, na rozdíl od „studených” (cold) toků vytvářených přes flow { }, které se spouštějí při objevení odběratele.
StateFlow byl stabilizován v kotlinx.coroutines 1.3.7 (prosinec 2020) a doporučen Googlem jako náhrada LiveData od Google I/O 2021. K lednu 2025 podle průzkumu JetBrains používá 56% nových Android projektů na Kotlin StateFlow jako hlavní reaktivní kontejner.
Volba mezi StateFlow a LiveData závisí na architektuře projektu, technologickém stacku a požadavcích na platformní nezávislost. Níže je srovnání podle šesti klíčových kritérií.
| Kritérium | StateFlow | LiveData |
|---|---|---|
| Platforma | Kotlin Multiplatform (Android, iOS, server) | Pouze Android |
| Lifecycle-aware | Ne — vyžaduje repeatOnLifecycle() | Ano — vestavěné vázání |
| Korutiny | Plná podpora (map, filter, combine) | Přes liveData { } builder |
| Null-bezpečnost | Ano — serializuje se přes kotlinx.serialization | Ano — přes nullability LiveData<String?> |
| Conflation | Conflated — přeskočí mezilehlé hodnoty | Pouze přes postValue() |
| Testování | runTest + Turbine nebo vestavěné operátory | InstantTaskExecutorRule + observeForever |
StateFlow vyžaduje explicitní správu odběru ve vrstvě View: ve Fragment/Activity se odběr provádí přes repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. To poskytuje větší kontrolu než automatický odběr LiveData, ale přidává šablonový kód. V Jetpack Compose se odběr zjednodušuje na val state by viewModel.uiState.collectAsState().
Doporučení Google (Android Developers, 2025): pro nové projekty na Kotlin používejte StateFlow, zejména při práci s Compose. LiveData ponechte pro: (1) Java kód, (2) knihovny vyžadující kompatibilitu s Java, (3) Room DAO (LiveData jako návratový typ DAO je stále populární).
MutableStateFlow — měnitelná verze StateFlow s otevřenou property value pro zápis. Analogicky k MutableLiveData se MutableStateFlow používá uvnitř ViewModel a publikuje se jako StateFlow (read-only) pro externí odběratele.
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()
}
}
Vlastnosti MutableStateFlow: (1) hodnota je vždy non-null — vyžaduje inicializaci přes konstruktor; (2) porovnání starých a nových hodnot přes equals() — pokud je nová hodnota rovna staré, odběratelé NEJSOU upozorněni; (3) zápis do value je možný z libovolného vlákna, ale blokuje volající vlákno pouze na krátkou dobu CAS operace. Podle dokumentace Kotlin Coroutines snižuje porovnání přes equals() počet zbytečných oznámení o 90% ve srovnání s LiveData — to přináší zvýšení výkonu při vysoké frekvenci aktualizací.
Při použití StateFlow ve ViewModel dodržujte následující pravidla: (1) používejte MutableStateFlow s modifikátorem private uvnitř ViewModel; (2) publikujte read-only StateFlow přes get(); (3) pro složité obrazovky používejte sealed class jako stav; (4) vyhněte se emisi hodnoty rovné aktuální (StateFlow to dělá automaticky).
// Doporučená struktura stavu obrazovky
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")
}
}
}
}
Použití sealed class jako jednotného typu stavu je doporučený přístup Google (UDF — Unidirectional Data Flow). Zaručuje, že UI je vždy v konzistentním stavu: Loading, Success nebo Error, ale ne současně. V IT Sectr jsme přešli na StateFlow + sealed class pro všechny obrazovky v roce 2022 — to zjednodušilo testování ViewModel o 40% díky předvídatelným stavům.
stateIn() — operátor převádějící studený Flow na horký StateFlow. Vyžaduje uvedení CoroutineScope (kde se spouští vnitřní korutina) a strategie SharingStarted. Správný výběr SharingStarted kriticky ovlivňuje výkon a životní cyklus StateFlow.
// Tři strategie SharingStarted:
// 1. SharingStarted.Eagerly — spouští se okamžitě, nezastavuje se
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — spouští se při prvním odběrateli, nezastavuje se
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — spouští se při odběratelích,
// zastavuje se po stopTimeoutMillis (výchozí 0) po odchodu posledního
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — optimální strategie pro ViewModel: po odchodu posledního odběratele vnitřní korutina pokračuje v práci ještě 5 sekund. Pokud se uživatel vrátí na obrazovku během této doby, odběr se obnoví bez restartování toku. Timeout zabraňuje častým restartům při rychlém přepínání mezi obrazovkami. Podle testů Google (Android Performance, 2024) snižuje WhileSubscribed s timeoutem 5 sekund spotřebu CPU o 25% ve srovnání s Eagerly.
Plnohodnotná vyhledávací obrazovka s vyhledávacím dotazem, výsledky a stavem načítání. ViewModel používá sealed class UIState a StateFlow pro reaktivní komunikaci s 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
}
}
// V Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... UI reagující na stavy Loading, Results, Error
}
Room (od verze 2.4.0) podporuje návrat Flow z DAO. Kombinování více Flow přes combine je mocný vzor pro složité obrazovky.
@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 automaticky sleduje změny v tabulce orders a znovu dotazuje data při jakýchkoli změnách. StateFlow + Room — moderní náhrada za dvojici Room + LiveData. Podle Google (Android Architecture Guide, 2025) je dvojice Flow + StateFlow + Room doporučena pro všechny projekty na Kotlin, kde je vyžadována reaktivní aktualizace UI při změně databáze.
Často kladené otázky
Konflace — mechanismus, při kterém StateFlow uchovává pouze poslední odeslanou hodnotu. Pokud je nová hodnota odeslána dříve, než odběratel zpracoval předchozí, mezilehlá hodnota se ztratí. To je důležité pro UI: pokud se stav mění z Loading → Success → Error a UI nestihl vykreslit Success, okamžitě přejde do Error bez zbytečného renderování. Konflace je klíčová optimalizace Androidu zabraňující nadměrným rekompozicím v Compose.
Použijte extension funkci liveData.asFlow() z knihovny lifecycle-livedata-ktx, poté .stateIn() pro převod na StateFlow. Opačná konverze — stateFlow.asLiveData(). Konverze je užitečná při migraci z LiveData na StateFlow: můžete postupně převádět ViewModel na StateFlow a ponechat starému View odběr přes LiveData.
StateFlow musí mít vždy hodnotu — to je kontrakt rozhraní: každý nově připojený odběratel okamžitě obdrží aktuální stav bez čekání. Počáteční hodnota se předává do konstruktoru MutableStateFlow(initialValue) nebo do operátoru stateIn(initialValue). Pokud stav může chybět, použijte MutableStateFlow<T?>(null) s nullable typem a zpracujte null v UI.
Ano, StateFlow je thread-safe: zápis a čtení value používají atomické operace (CAS). Nicméně collect() je suspend funkce a musí být spuštěna v korutině. Pokud jsou emise a collect prováděny na různých vláknech, StateFlow zaručuje happens-before pro všechny operace s value. Pro sběr StateFlow ve View použijte lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Neexistují žádná omezení, ale doporučuje se nejvýše 3-5 samostatných StateFlow na obrazovku. Pokud je potřeba více různých stavů, spojte je do jednoho přes sealed class nebo data class. Každý StateFlow vyžaduje přidělení objektu Continuation při sběru — stovka StateFlow může vytvořit znatelné zatížení GC. Podle doporučení Google je jeden sealed class UIState na obrazovku optimální rovnováha mezi čitelností a výkonem.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také