Skip to Content
Course content

183: StateFlow vs SharedFlow Use Cases Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I keep seeing the same pattern in pull requests: developers using StateFlow for everything because it's "easier" to observe in the UI, or using SharedFlow for everything because they're afraid of state repetition. It's a common point of friction. To get this straight, let's actually build a small piece of a payment processing screen and intentionally break it.

The "Everything is State" Trap

Let's start with a PaymentViewModel. I want to track the current balance and notify the user when a payment is successful. My first instinct—and the mistake I see most often—is to treat the "Success" message as a piece of state.

class PaymentViewModel : ViewModel() {
    private val _balance = MutableStateFlow(100.0)
    val balance = _balance.asStateFlow()

    private val _paymentStatus = MutableStateFlow<String?>(null)
    val paymentStatus = _paymentStatus.asStateFlow()

    fun makePayment(amount: Double) {
        _balance.value -= amount
        _paymentStatus.value = "Payment of $$amount successful!"
    }
}

On the surface, this looks fine. But let's imagine the UI. The user clicks "Pay," the paymentStatus updates, and a Toast pops up saying "Payment successful!". Then, the user rotates their phone. In Android, the Activity is recreated, and the UI re-collects the StateFlow. Since StateFlow always emits the current state to new collectors, the Toast pops up again. That's a terrible user experience. We're treating a "one-time event" as "persistent state."

Switching to Events with SharedFlow

Okay, so the status isn't a state; it's an event. I'll swap StateFlow for SharedFlow. I don't want any replay here because if the user rotates the screen, I don't want that success message firing again.

class PaymentViewModel : ViewModel() {
    private val _balance = MutableStateFlow(100.0) // Balance is still state
    val balance = _balance.asStateFlow()

    private val _paymentEvents = MutableSharedFlow<String>()
    val paymentEvents = _paymentEvents.asSharedFlow()

    fun makePayment(amount: Double) {
        _balance.value -= amount
        viewModelScope.launch {
            _paymentEvents.emit("Payment of $$amount successful!")
        }
    }
}

Now, the rotation problem is gone. The event is fired, the Toast shows, and when the screen rotates, the new collector finds nothing in the buffer. But wait—I've just introduced a new bug. What happens if the payment completes while the app is momentarily in the background or during a configuration change? The event is emitted, but there's no active collector. The event simply vanishes into the void. The user never knows their payment worked.

Finding the Middle Ground with Replay

This is where we start tuning. I want the event to be "sticky" just long enough for the UI to catch it, but not so sticky that it persists forever like a StateFlow. I can use the replay parameter in SharedFlow.

private val _paymentEvents = MutableSharedFlow<String>(replay = 1)

Now, the last emitted event is cached. If the user rotates the screen immediately after the payment, the new collector gets the last success message. But there's a catch: SharedFlow with replay = 1 is essentially a StateFlow that doesn't require an initial value. However, unlike StateFlow, it doesn't have a .value property you can check synchronously.

I've realized that for "Events" (like navigation or snackbars), we actually have to decide: is this an event that must be processed exactly once, or is it a state that the user needs to see until it's acknowledged? If it's the latter, I actually go back to StateFlow but I manually "reset" the state to null after the UI handles it.

The Final Verdict on Use Cases

After playing with these two, here is how I now categorize them in my head to avoid the back-and-forth:

  • StateFlow: Use this for "The Truth." The current user profile, the current list of items in a cart, the loading status. If a new screen opens and asks "What is the current value?", StateFlow has the answer.
  • SharedFlow: Use this for "The Broadcast." A WebSocket message arriving, a global logout signal, or a trigger to play a sound effect. These are things that happen at a point in time rather than things that are.

If you find yourself fighting with SharedFlow's missing .value or StateFlow's sticky repetitions, you're likely using the wrong tool for that specific piece of data.




📋 Practical Task

Implementing a Notification System for a Real-time Order Tracker

You are building a food delivery tracking screen. You need to implement two different streams of data in a OrderViewModel:

  1. Order Status: (e.g., "Preparing", "Out for Delivery", "Delivered"). This should be accessible immediately when the user opens the screen and should survive configuration changes without re-triggering "event" logic.
  2. Driver Alerts: (e.g., "Driver is outside!", "Driver is calling you"). These are urgent notifications that should be shown as a popup. If the user isn't looking at the screen when they happen, they should still see the most recent alert when they return, but they shouldn't see the same alert again if they rotate the device after dismissing it.

Requirements:

  • Implement the OrderViewModel using the most appropriate Flow type for each requirement.
  • For the Driver Alerts, ensure you use a configuration that allows the last alert to be cached for new collectors, but differentiate it from a standard StateFlow.
  • Write a small test function or a main block that simulates:
    1. Updating the order status.
    2. Emitting a driver alert.
    3. Collecting both flows to verify the behavior.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.