Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
183: StateFlow vs SharedFlow Use Cases Revisited
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?",
StateFlowhas 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:
- 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.
- 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
OrderViewModelusing 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
mainblock that simulates:- Updating the order status.
- Emitting a driver alert.
- Collecting both flows to verify the behavior.
There are no comments for now.