0Pricing
Kotlin Academy · Lesson

StateFlow: Hot State Holder for UI

Use StateFlow as a reactive state container and observe it in ViewModels.

StateFlow: Hot State Holder for UI is a free Kotlin Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is StateFlow?

StateFlow is a hot, state-holding Flow that always has a value and emits updates to all collectors. It replaces LiveData in modern Kotlin architectures.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val stateFlow = MutableStateFlow(0)  // initial value
    println("Current: ${stateFlow.value}")
    stateFlow.value = 42
    println("Updated: ${stateFlow.value}")
}

MutableStateFlow vs StateFlow

MutableStateFlow is the mutable implementation used internally. Expose StateFlow (read-only) to external observers by casting or using .asStateFlow().

import kotlinx.coroutines.flow.*
class CounterViewModel {
    private val _count = MutableStateFlow(0)  // mutable internally
    val count: StateFlow<Int> = _count.asStateFlow()  // read-only externally
    fun increment() { _count.value++ }
    fun decrement() { _count.value-- }
}

Collecting StateFlow

Collect StateFlow like any other Flow. The latest value is emitted immediately on collection.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val state = MutableStateFlow("loading")
    launch {
        state.collect { println("State: $it") }
    }
    delay(50); state.value = "success"
    delay(50); state.value = "idle"
    delay(50)
    coroutineContext.cancelChildren()
}

StateFlow is Conflated

StateFlow only emits when the value changes. If you set the same value twice, only one emission occurs — it conflates duplicate consecutive values.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = MutableStateFlow("A")
    launch {
        flow.collect { println("Received: $it") }
    }
    delay(50)
    flow.value = "A"  // no emission — same value
    flow.value = "B"  // emits
    flow.value = "B"  // no emission — same value
    flow.value = "C"  // emits
    delay(50)
    coroutineContext.cancelChildren()
}

StateFlow in ViewModel

The canonical ViewModel pattern: private MutableStateFlow mutated by business logic, public StateFlow observed by UI.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
sealed class UiState { object Loading : UiState(); data class Success(val data: String) : UiState(); data class Error(val msg: String) : UiState() }
class MyViewModel(scope: CoroutineScope) {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    init {
        scope.launch {
            delay(100)  // simulate load
            _uiState.value = UiState.Success("Hello!")
        }
    }
}

update() for Atomic Mutation

Use update { } to atomically update the state based on the current value — important for concurrent updates.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val counter = MutableStateFlow(0)
    List(100) {
        launch(Dispatchers.Default) {
            counter.update { it + 1 }  // thread-safe atomic update
        }
    }.forEach { it.join() }
    println("Counter: ${counter.value}")  // 100
}

compareAndSet for Optimistic Updates

compareAndSet(expect, update) only sets the new value if the current value matches expect. Useful for optimistic concurrency.

import kotlinx.coroutines.flow.*
fun main() {
    val state = MutableStateFlow("idle")
    val changed = state.compareAndSet("idle", "loading")
    println("Changed: $changed, Value: ${state.value}")  // true, loading
    val changed2 = state.compareAndSet("idle", "error")  // won't change
    println("Changed: $changed2, Value: ${state.value}") // false, loading
}

StateFlow vs LiveData

StateFlow works without Android lifecycle. LiveData is lifecycle-aware but Android-only. Prefer StateFlow for shared KMP ViewModels.

import kotlinx.coroutines.flow.*
// StateFlow: pure Kotlin, works in commonMain
// val state = MutableStateFlow("value")

// LiveData: Android-only, lifecycle-aware
// val liveData = MutableLiveData("value")

// In Compose, collect StateFlow with:
// val state by viewModel.uiState.collectAsState()
fun main() { println("StateFlow: multiplatform; LiveData: Android-only") }

stateIn: Converting Cold Flow to StateFlow

flow.stateIn(scope, started, initialValue) converts a cold Flow to a hot StateFlow, sharing one subscription among all collectors.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = flow {
        println("Fetching...")
        delay(100)
        emit("data")
    }
    val hot: StateFlow<String> = cold.stateIn(
        scope = this,
        started = SharingStarted.Lazily,
        initialValue = "loading"
    )
    launch { hot.collect { println("A: $it") } }
    launch { hot.collect { println("B: $it") } }
    delay(200)
    coroutineContext.cancelChildren()
}

SharingStarted Strategies

Eagerly: starts immediately. Lazily: starts on first collector. WhileSubscribed(stopTimeout): stops when no collectors, restarts when one subscribes.

import kotlinx.coroutines.flow.*
// Eagerly: upstream starts right away
// SharingStarted.Eagerly

// Lazily: waits for first subscriber
// SharingStarted.Lazily

// WhileSubscribed: stops 5s after last subscriber
// SharingStarted.WhileSubscribed(5000)

// Typical ViewModel usage:
// val uiState = repo.dataFlow.stateIn(viewModelScope, WhileSubscribed(5000), Loading)

Observing Derived State

Derive a new StateFlow from an existing one using map + stateIn. The derived flow updates whenever the source changes.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val count = MutableStateFlow(5)
    val doubled = count.map { it * 2 }.stateIn(this, SharingStarted.Eagerly, 10)
    println(doubled.value)  // 10
    count.value = 7
    delay(50)
    println(doubled.value)  // 14
    coroutineContext.cancelChildren()
}

Quick Check

What does StateFlow do when you set the same value twice?

Recap

StateFlow is a hot, always-valued, conflated Flow. Use MutableStateFlow internally and expose StateFlow externally. Use update() for concurrent mutations and stateIn() to convert cold flows.

Frequently asked questions

Is the “StateFlow: Hot State Holder for UI” lesson free?

Yes — the full text of “StateFlow: Hot State Holder for UI” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.

What will I learn in “StateFlow: Hot State Holder for UI”?

Use StateFlow as a reactive state container and observe it in ViewModels. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Kotlin Academy?

No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “StateFlow: Hot State Holder for UI” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Kotlin Academy lesson?

Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. StateFlow: Hot State Holder for UI
  2. SharedFlow: Event Buses and One-Shot Events
  3. Converting Cold Flow to Hot with shareIn and stateIn
  4. Testing StateFlow and SharedFlow with Turbine
← Back to Kotlin Academy