Mastering State Management in Kotlin/Android: LiveData vs Flow vs StateFlow

Understand the differences, use cases, and best practices for managing reactive data in Android development

Dhananjay Trivedi
7 min read1 day ago

1. LiveData

Overview:

  • LiveData is part of Android’s Architecture Components (Outdated)
  • It is lifecycle-aware, meaning it respects the lifecycle of UI components (e.g., Activities, Fragments).
  • Observers are only notified when the associated lifecycle is in an active state (STARTED or RESUMED).

Use Cases:

  • UI-related data that needs to be observed by lifecycle-aware components (e.g., updating a TextView when data changes).
  • Simple one-time event handling in the UI (e.g., navigation or showing a snackbar).

Advantages:

  • Lifecycle-aware: Reduces the risk of memory leaks.
  • Easy to use: Designed for ViewModels and UI layers in Android.
  • Integration with DataBinding: Works seamlessly with Android’s DataBinding.

Disadvantages:

  • Static nature: Cannot handle streams of data (e.g., continuous updates or data transformations) efficiently.
  • Limited to the main thread: Primarily designed for UI-bound data and doesn’t work well with multi-threaded operations.
  • No backpressure handling: Cannot deal with high-frequency data updates gracefully.

Example

ViewModel:

class MyViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data

fun updateData(newValue: String) {
_data.value = newValue
}
}

Activity or Fragment:

class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Observe LiveData
viewModel.data.observe(this) { newValue ->
findViewById<TextView>(R.id.textView).text = newValue
}

// Update LiveData
viewModel.updateData("Hello, LiveData!")
}
}

Key Points:

  • LiveData is lifecycle-aware, so you don’t need to worry about unsubscribing observers.
  • Best suited for UI-bound data in legacy projects.

# Bonus Extra Theory — Cold Stream vs Hot Stream in Reactive Programming

To better understand Flow, StateFlow, MutableState we must be clear with what Hot Stream and Cold Stream is.

Cold Streams

In a cold stream, the data source (or producer) is inactive until there is a subscriber (or collector). Each subscriber receives its own independent data stream from the beginning of the stream.

Characteristics:

  • The producer starts emitting data only when subscribed to.
  • Multiple subscribers receive separate, independent emissions.

Examples:

Flow, RxJava Observable (depending on configuration).

Analogy:

Think of a cold drink vending machine — it serves a fresh drink only when someone requests it.

Code Example:

val coldStream = flow {
emit(1)
emit(2)
emit(3)
}

// Subscription 1
coldStream.collect { value ->
println("Subscriber 1 received: $value")
}

// Subscription 2
coldStream.collect { value ->
println("Subscriber 2 received: $value")
}

Hot Stream:

In a hot stream, the data source (or producer) starts emitting data regardless of whether there are subscribers. Subscribers can join at any time and receive data from the current state onward.

Characteristics:

  • The producer is always active and emits data continuously.
  • New subscribers only receive data emitted from the point they subscribe (no replay unless explicitly implemented).

Examples:

StateFlow, SharedFlow, RxJava Subject.

Analogy:

Think of a live radio broadcast — you hear the show only from the moment you tune in.

Code Example:

val hotStream = MutableSharedFlow<Int>() // A hot stream

// Producer emitting data
lifecycleScope.launch {
repeat(5) {
delay(1000)
hotStream.emit(it)
}
}

// Subscriber 1
lifecycleScope.launch {
hotStream.collect { value ->
println("Subscriber 1 received: $value")
}
}

// Subscriber 2 (joins late)
lifecycleScope.launch {
delay(3000)
hotStream.collect { value ->
println("Subscriber 2 received: $value")
}
}

Output:

  • Subscriber 1 receives: 0, 1, 2, 3, 4.
  • Subscriber 2 (joins late) receives: 3, 4.

The main distinction is subscription behavior:

  • Cold Stream: Restarts for each new subscriber, ensuring each gets its own timeline of data.
  • Hot Stream: Shares the same timeline of data for all subscribers.

Now back to our main story.

2. Flow

  • Part of Kotlin’s Coroutines library.
  • A Cold Stream: Data is emitted only when there is an active collector.
  • Supports asynchronous and streaming data, making it ideal for non-UI scenarios.

Use Cases:

  • Handling continuous streams of data (e.g., periodic network polling or database updates).
  • Non-UI data processing in repositories or other layers (e.g., fetch data from a remote source and transform it before delivering it to the UI).
  • Reactive programming: When you need transformations like map, filter, or flatMap.

Advantages:

  • Backpressure support: Can handle high-frequency data updates.
  • Flexible and powerful: Can be used with various data streams, including network calls and database queries.
  • Supports operators: Provides operators like map, combine, and flatMap for data manipulation.
  • Threading flexibility: Works well in multithreaded environments.

Disadvantages:

  • Not lifecycle-aware: Unlike LiveData, Flow doesn’t automatically manage subscriptions based on lifecycle states.
  • Requires manual collection: Data must be explicitly collected (e.g., using collect() or collectLatest() in coroutines).
  • Steeper learning curve: Slightly more complex for beginners compared to LiveData.

Code Example

Repository Layer

class MyRepository {
fun fetchData(): Flow<String> = flow {
for (i in 1..5) {
emit("Item $i") // Emit new data
delay(1000) // Simulate data stream
}
}
}

ViewModel

class MyViewModel(private val repository: MyRepository) : ViewModel() {
val data: Flow<String> = repository.fetchData()
}

Activity or Fragment

class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Launching with lifecycle scope
// To make the collector lifecycle-aware
lifecycleScope.launch {
viewModel.data.collect { newValue ->
findViewById<TextView>(R.id.textView).text = newValue
}
}
}
}

Key Points:

  • Flow provides backpressure support and is ideal for streaming data.
  • You must handle collection explicitly, often using lifecycleScope or viewModelScope.

3. State — StateFlow and MutableState

State in this context typically refers to StateFlow (part of Kotlin's Coroutines)

and

Compose’s MutableState (part of Jetpack Compose).

Overview:

  • StateFlow: A hot stream that emits the most recent value to new subscribers. It’s designed to replace LiveData in modern Kotlin-based applications.
  • MutableState (Jetpack Compose): A Compose-specific tool for managing UI state.

Use Cases:

  • StateFlow: Suitable for scenarios where you need a lifecycle-agnostic, observable data holder (e.g., shared state between ViewModel and UI).
  • MutableState (Compose): Designed for Jetpack Compose to handle UI state within the composition.
  • Managing UI state in Compose or scenarios requiring state retention across configuration changes.

Advantages:

StateFlow:

  • Lifecycle-agnostic: Can be used anywhere, not just the UI layer.
  • Combines well with Flow operators for data manipulation.

MutableState:

  • Seamlessly integrates with Jetpack Compose.
  • Automatically recomposes UI when the state changes.

Consistent data: Always emits the latest value (useful for state management).

Disadvantages:

  • StateFlow: Lacks lifecycle-awareness, requiring manual handling of subscriptions in lifecycle-bound components.
  • MutableState: Limited to Compose and not reusable outside its context.

Code Sample for StateFlow

ViewModel

class MyViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow("Initial State")
val stateFlow: StateFlow<String> = _stateFlow

fun updateState(newValue: String) {
_stateFlow.value = newValue
}
}

UI:

class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

lifecycleScope.launch {
viewModel.stateFlow.collect { newValue ->
findViewById<TextView>(R.id.textView).text = newValue
}
}

viewModel.updateState("Hello, StateFlow!")
}
}

Key Points:

  • StateFlow always holds the most recent value, making it suitable for state management.
  • It is not lifecycle-aware, so you must use lifecycleScope or manage subscriptions manually.

Code Sample for Mutable State

ViewModel

class MyViewModel : ViewModel() {
var text by mutableStateOf("Initial Text")
private set

fun updateText(newValue: String) {
text = newValue
}
}

UI

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val text = viewModel.text

Column {
Text(text = text)
Button(onClick = { viewModel.updateText("Hello, Compose!") }) {
Text("Update Text")
}
}
}

Key Points:

  • MutableState integrates seamlessly with Jetpack Compose.
  • Automatically triggers recomposition when the state changes.

Still Confused between StateFlow vs Flow? Read on:

While Flow and StateFlow are both part of Kotlin’s reactive programming model, they serve different purposes, especially when it comes to communication between the ViewModel and the View in Android development.

Advantages of StateFlow in ViewModel to View Communication:

  • StateFlow is designed specifically for representing state and holding a single up-to-date value. This makes it ideal for cases where the ViewModel needs to hold and emit the current state to the View.
  • Flow, on the other hand, is typically used for a stream of values and doesn’t inherently hold a “current” value (unless configured with stateIn or similar).
// StateFlow in ViewModel
private val _state = MutableStateFlow("Initial State")
val state: StateFlow<String> = _state

// Update state
_state.value = "Updated State"

Hot Stream Nature:

  • StateFlow is hot. Once the state is updated in the ViewModel, any observer (like the UI) that collects the StateFlow will always receive the latest value immediately. Even if the UI collects late, it will get the current/latest value right away.
  • In contrast, Flow is cold, meaning that you need to start a collection for it to emit values, and it doesn’t retain the latest emitted value after the collection ends.
// Flow is cold
val coldFlow = flow {
emit("Some data")
}
// Starts emitting only when collected

Lifecycle-Awareness:

  • StateFlow works well with lifecycle-awareness, especially when used in conjunction with lifecycleScope or repeatOnLifecycle. Since it's hot, you don't need to worry about losing the latest state if a new collector subscribes. The UI always gets the most recent state immediately when it collects.
  • Flow, though flexible, requires manual management of lifecycle events to avoid collecting data after the UI component is destroyed.
// Collect StateFlow in lifecycle-aware scope
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
// Update UI with the latest state
}
}
}

When to Use Flow vs StateFlow:

  • StateFlow: Use when you need to represent and observe a single state that is updated over time (e.g., loading state, user data). It’s ideal for scenarios where you want the ViewModel to always emit the current state to the UI.
  • Flow: Use when you are dealing with a stream of values or events over time (e.g., network responses, user interactions) and do not need to retain the last emitted value when the collector is inactive.

Summary:

  • StateFlow is better suited for managing and observing UI states in a lifecycle-aware manner. It ensures that the UI always receives the latest value as soon as it starts collecting.
  • Flow is more suitable for asynchronous streams of data where values are emitted over time and the order or current value isn’t the primary concern.

Thanks for reading!
:)

--

--

Dhananjay Trivedi
Dhananjay Trivedi

No responses yet