StateFlow — reaktiv tillståndsbehållare från Kotlin Coroutines-biblioteket, som är StateFlow<T> — en undertyp av Flow som alltid håller det aktuella värdet och skickar det till nya prenumeranter. Förklarar kärnan i StateFlow: till skillnad från LiveData är StateFlow inte bundet till Android-ramverket och fungerar på vilken Kotlin-plattform som helst. Enligt Google (Android Developers, 2025) rekommenderas StateFlow som det främsta alternativet till LiveData för nya projekt i ren Kotlin, särskilt i MVVM-arkitektur med Jetpack Compose.
Huvudpunkter
collectAsState() i Compose eller repeatOnLifecycle() i View.StateFlow — är ett gränssnitt från biblioteket kotlinx.coroutines.flow som utökar MutableSharedFlow med den fasta parametern replay = 1. Detta innebär att StateFlow alltid kommer ihåg det senast skickade värdet och omedelbart spelar upp det för varje ny prenumerant. Till skillnad från LiveData är StateFlow en del av standardbiblioteket Kotlin Coroutines och har inga beroenden till Android.
Konceptuellt är StateFlow en reaktiv egenskap: du läser dess aktuella värde via .value och prenumererar på ändringar via .collect(). Denna modell kallas "varm" ström (hot flow) — datakällan är aktiv oberoende av om det finns prenumeranter, till skillnad från "kalla" (cold) strömmar som skapas via flow { } och startas när en prenumerant dyker upp.
StateFlow stabiliserades i kotlinx.coroutines 1.3.7 (december 2020) och rekommenderades av Google som ersättning för LiveData från och med Google I/O 2021. I januari 2025, enligt JetBrains enkät, använder 56% av nya Android-projekt i Kotlin StateFlow som primär reaktiv behållare.
Valet mellan StateFlow och LiveData beror på projektets arkitektur, teknologistack och krav på plattformsoberoende. Nedan följer en jämförelse enligt sex viktiga kriterier.
| Kriterium | StateFlow | LiveData |
|---|---|---|
| Plattform | Kotlin Multiplatform (Android, iOS, server) | Endast Android |
| Lifecycle-aware | Nej — kräver repeatOnLifecycle() | Ja — inbyggd bindning |
| Korutiner | Fullt stöd (map, filter, combine) | Via liveData { } builder |
| Null-säkerhet | Ja — serialiseras via kotlinx.serialization | Ja — via nullability LiveData<String?> |
| Konflation | Konflaterad — hoppar över mellanliggande värden | Endast via postValue() |
| Testning | runTest + Turbine eller inbyggda operatorer | InstantTaskExecutorRule + observeForever |
StateFlow kräver explicit prenumerationshantering i View-lagret: i Fragment/Activity utförs prenumeration via repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Detta ger mer kontroll än LiveDatas automatiska prenumeration, men lägger till mallkod. I Jetpack Compose förenklas prenumerationen till val state by viewModel.uiState.collectAsState().
Googles rekommendation (Android Developers, 2025): för nya projekt i Kotlin, använd StateFlow, särskilt när du arbetar med Compose. Behåll LiveData för: (1) Java-kod, (2) bibliotek som kräver Java-kompatibilitet, (3) Room DAO (LiveData som DAO-returtyp är fortfarande populär).
MutableStateFlow — föränderlig version av StateFlow med öppen property value för skrivning. I analogi med MutableLiveData används MutableStateFlow inuti ViewModel och publiceras som StateFlow (skrivskyddad) för externa prenumeranter.
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()
}
}
Egenskaper för MutableStateFlow: (1) värdet är alltid icke-null — kräver initiering via konstruktor; (2) jämförelse av gamla och nya värden via equals() — om det nya värdet är lika med det gamla, meddelas INTE prenumeranter; (3) skrivning till value är möjlig från vilken tråd som helst, men blockerar den anropande tråden endast under den korta CAS-operationen. Enligt Kotlin Coroutines dokumentation minskar jämförelse via equals() antalet onödiga meddelanden med 90% jämfört med LiveData — detta ger prestandaökning vid hög uppdateringsfrekvens.
När du använder StateFlow i ViewModel, följ dessa regler: (1) använd MutableStateFlow med private-modifierare inuti ViewModel; (2) publicera skrivskyddad StateFlow via get(); (3) för komplexa skärmar, använd sealed class som tillstånd; (4) undvik att skicka ett värde som är lika med det aktuella (StateFlow gör detta automatiskt).
// Rekommenderad skärmtillståndsstruktur
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")
}
}
}
}
Användning av sealed class som enhetlig tillståndstyp är Googles rekommenderade metod (UDF — Unidirectional Data Flow). Den garanterar att UI alltid är i ett konsekvent tillstånd: Loading, Success eller Error, men inte samtidigt. På IT Sectr bytte vi till StateFlow + sealed class för alla skärmar 2022 — detta förenklade ViewModel-testning med 40% tack vare förutsägbara tillstånd.
stateIn() — operator som omvandlar en kall Flow till en varm StateFlow. Den kräver angivelse av CoroutineScope (där den interna korutinen startas) och SharingStarted-strategi. Rätt val av SharingStarted påverkar kritiskt prestanda och livscykel för StateFlow.
// Tre strategier för SharingStarted:
// 1. SharingStarted.Eagerly — startar omedelbart, stoppas inte
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — startar vid första prenumeranten, stoppas inte
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — startar när det finns prenumeranter,
// stoppas efter stopTimeoutMillis (standard 0) efter att den senaste lämnat
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — optimal strategi för ViewModel: efter att den senaste prenumeranten lämnat fortsätter den interna korutinen att arbeta i ytterligare 5 sekunder. Om användaren återvänder till skärmen inom denna tid återställs prenumerationen utan att strömmen startas om. Timeouten förhindrar frekventa omstarter vid snabb växling mellan skärmar. Enligt Googles tester (Android Performance, 2024) minskar WhileSubscribed med 5 sekunders timeout CPU-förbrukningen med 25% jämfört med Eagerly.
Fullständig sökskärm med sökfråga, resultat och laddningstillstånd. ViewModel använder sealed class UIState och StateFlow för reaktiv kommunikation med 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
}
}
// I Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... UI som reagerar på tillstånden Loading, Results, Error
}
Room (från och med version 2.4.0) stöder retur av Flow från DAO. Att kombinera flera Flow via combine är ett kraftfullt mönster för komplexa skärmar.
@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 spårar automatiskt ändringar i tabellen orders och begär om data vid eventuella ändringar. StateFlow + Room — modern ersättning för kombinationen Room + LiveData. Enligt Google (Android Architecture Guide, 2025) rekommenderas kombinationen Flow + StateFlow + Room för alla Kotlin-projekt som kräver reaktiv UI-uppdatering vid databasändringar.
Vanliga frågor
Konflation — mekanism där StateFlow endast behåller det senast skickade värdet. Om ett nytt värde skickas innan prenumeranten har behandlat det föregående, går mellanvärdet förlorat. Detta är viktigt för UI: om tillståndet ändras från Loading → Success → Error och UI inte hann rendera Success, går det direkt till Error utan onödig rendering. Konflation är en viktig Android-optimering som förhindrar överflödiga recompositioner i Compose.
Använd extension-funktionen liveData.asFlow() från biblioteket lifecycle-livedata-ktx, sedan .stateIn() för att konvertera till StateFlow. Omvänd konvertering — stateFlow.asLiveData(). Konvertering är användbar vid migrering från LiveData till StateFlow: du kan gradvis flytta ViewModel till StateFlow och låta den gamla View prenumerera via LiveData.
StateFlow måste alltid ha ett värde — detta är gränssnittets kontrakt: varje nyansluten prenumerant får omedelbart det aktuella tillståndet utan väntan.initiala värdet skickas till konstruktorn MutableStateFlow(initialValue) eller till operatorn stateIn(initialValue). Om tillståndet kan saknas, använd MutableStateFlow<T?>(null) med nullable-typ och hantera null i UI.
Ja, StateFlow är trådsäker: skrivning och läsning av value använder atomära operationer (CAS). Dock är collect() en suspend-funktion och måste startas i en korutin. Om emission och collect utförs på olika trådar garanterar StateFlow happens-before för alla value-operationer. För att samla StateFlow i View, använd lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Det finns inga begränsningar, men det rekommenderas inte mer än 3-5 separata StateFlow per skärm. Om fler olika tillstånd behövs, kombinera dem till ett via sealed class eller data class. Varje StateFlow kräver allokering av ett Continuation-objekt vid insamling — hundra StateFlow kan skapa en märkbar belastning på GC. Enligt Googles rekommendation är en sealed class UIState per skärm den optimala balansen mellan läsbarhet och prestanda.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också