Kotlin StateFlow and SharedFlow

Regular Flow is cold — it only runs when collected. StateFlow and SharedFlow are hot flows that are always active, can have multiple collectors at the same time, and emit values regardless of whether anyone is listening. They are the foundation of reactive UI state in Android apps.

Cold Flow vs Hot Flow


Cold Flow (regular flow):
  Each collector gets its own independent sequence.
  Flow only runs when collected.
  Like a YouTube video — each viewer starts from the beginning.

Hot Flow (StateFlow / SharedFlow):
  All collectors share the same stream of values.
  Values are emitted regardless of collectors.
  Like a live TV broadcast — viewers join mid-stream.

StateFlow — State Holder

StateFlow always holds a current value. New collectors immediately receive the latest value. It replaces LiveData in modern Android development.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    // MutableStateFlow holds and updates state
    val counter = MutableStateFlow(0)

    // Collector 1 — receives all updates
    val job = launch {
        counter.collect { value ->
            println("Collector: count = $value")
        }
    }

    delay(100)
    counter.value = 1   // update state
    delay(100)
    counter.value = 2
    delay(100)
    counter.value = 3

    delay(100)
    job.cancel()
}

Output:

Collector: count = 0
Collector: count = 1
Collector: count = 2
Collector: count = 3

StateFlow in Android ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class UiState(val loading: Boolean = false, val data: String = "", val error: String? = null)

class MainViewModel : ViewModel() {

    // Private mutable — only ViewModel can change it
    private val _uiState = MutableStateFlow(UiState())

    // Public read-only — Activity/Fragment collects this
    val uiState: StateFlow = _uiState.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = UiState(loading = true)
            try {
                val result = fetchFromServer()
                _uiState.value = UiState(data = result)
            } catch (e: Exception) {
                _uiState.value = UiState(error = e.message)
            }
        }
    }

    private suspend fun fetchFromServer(): String {
        kotlinx.coroutines.delay(1000)
        return "Hello from server!"
    }
}

// In Activity:
// lifecycleScope.launch {
//     viewModel.uiState.collect { state ->
//         when {
//             state.loading -> showSpinner()
//             state.error != null -> showError(state.error)
//             else -> showData(state.data)
//         }
//     }
// }

SharedFlow — Event Bus

SharedFlow does not hold a single current value. It is used for one-time events like navigation, toasts, and dialogs where replaying the last value would be wrong.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val events = MutableSharedFlow()

    // Collector 1
    val job1 = launch {
        events.collect { println("Collector 1: $it") }
    }

    // Collector 2
    val job2 = launch {
        events.collect { println("Collector 2: $it") }
    }

    delay(100)
    events.emit("User logged in")
    delay(100)
    events.emit("Order placed")
    delay(100)

    job1.cancel()
    job2.cancel()
}

Output:

Collector 1: User logged in
Collector 2: User logged in
Collector 1: Order placed
Collector 2: Order placed

StateFlow vs SharedFlow


                  StateFlow             SharedFlow
───────────────────────────────────────────────────────
Has current value?  YES (always)         NO
New collector gets? Latest value         Nothing (by default)
Replay?             Always 1 value       Configurable (replay = N)
Emit same value?    Skipped (dedup)      Always emitted
Use for?            UI state             One-time events

SharedFlow with Replay

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    // replay = 2 means new collectors get the last 2 emitted values
    val notifications = MutableSharedFlow(replay = 2)

    notifications.emit("App started")
    notifications.emit("Database ready")
    notifications.emit("Server connected")

    // Late collector — receives last 2 replayed values
    notifications.collect { println("Late collector: $it") }
}

Output:

Late collector: Database ready
Late collector: Server connected

Practical Example: Cart State Manager

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

data class CartState(val items: List = emptyList(), val total: Int = 0)

class CartStore {
    private val _state = MutableStateFlow(CartState())
    val state: StateFlow = _state.asStateFlow()

    private val _events = MutableSharedFlow()
    val events: SharedFlow = _events.asSharedFlow()

    suspend fun addItem(item: String, price: Int) {
        val current = _state.value
        _state.value = CartState(current.items + item, current.total + price)
        _events.emit("Added: $item (₹$price)")
    }
}

fun main() = runBlocking {
    val cart = CartStore()

    launch { cart.state.collect  { println("State  → items=${it.items.size}, total=₹${it.total}") } }
    launch { cart.events.collect { println("Event  → $it") } }

    delay(50)
    cart.addItem("Laptop", 75000)
    cart.addItem("Mouse",  800)
    delay(200)
}

Leave a Comment

Your email address will not be published. Required fields are marked *