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
125: The Observer Pattern with Flow
I've spent a lot of time in older Java and Kotlin codebases where the "Observer Pattern" was implemented using a bunch of custom Listener interfaces. You know the drill: you create an OnUserChangedListener, maintain a MutableList of those listeners, and loop through them manually every time a value changes. It's tedious, and it's a breeding ground for memory leaks if you forget to remove a listener when a screen closes.
Let's try to build a simple User Profile system and see how we can replace that manual plumbing with Kotlin Flow.
The callback approach feels clunky
Imagine we have a UserProfileManager. I want the UI header and a settings page to update whenever the user's display name changes. If I do this the old-fashioned way, it looks something like this:
class UserProfileManager {
var displayName: String = "Guest"
set(value) {
field = value
listeners.forEach { it.onNameChanged(value) }
}
private val listeners = mutableListOf<NameChangeListener>()
fun addListener(l: NameChangeListener) { listeners.add(l) }
}
interface NameChangeListener {
fun onNameChanged(newName: String)
}
It works, but I hate it. I have to manage the list, I have to handle the interface, and the "observer" has no idea what the current name is until the first time it actually changes. If the user is already "Alice", the header stays as "Guest" until the name changes to "Bob". That's a bug waiting to happen.
Trying a simple Flow
I wonder if a standard Flow would work better. Let's try to emit the name whenever it changes. I'll use a MutableSharedFlow because I want multiple parts of the app to be able to "tune in" to these updates.
class UserProfileManager {
private val _nameFlow = MutableSharedFlow<String>()
val nameFlow = _nameFlow.asSharedFlow()
suspend fun updateName(newName: String) {
_nameFlow.emit(newName)
}
}
I'll set up two observers—our Header and our Settings page—to collect this flow. But wait, I just hit the same problem as before. I started the app, the UserProfileManager already had a name, but the Header is empty. Why? Because SharedFlow is "hot" and doesn't store the last emitted value. It only broadcasts new events to whoever is listening at that exact moment.
Finding the sweet spot with StateFlow
This is where StateFlow comes in. It's essentially a SharedFlow that always remembers the last value and requires an initial state. It's the perfect implementation of the Observer pattern for state management.
Let's rewrite the manager. I'm going to use a backing property to keep the mutation private while exposing a read-only StateFlow to the rest of the app.
class UserProfileManager {
private val _displayName = MutableStateFlow("Guest")
val displayName = _displayName.asStateFlow()
fun updateName(newName: String) {
_displayName.value = newName
}
}
Now, look what happens when I set up my observers. I'll use collect to listen for changes:
val manager = UserProfileManager()
// Observer 1: The Header
scope.launch {
manager.displayName.collect { name ->
println("Header updated to: $name")
}
}
// Observer 2: The Settings Page
scope.launch {
manager.displayName.collect { name ->
println("Settings page updated to: $name")
}
}
manager.updateName("Alice")
The magic here is that as soon as collect is called, the current value ("Guest") is immediately emitted to the observer. Then, when I call updateName("Alice"), both observers trigger instantly. I didn't have to write a single interface or manage a list of listeners. The Flow handles the subscription logic under the hood.
Wait, what about redundant updates?
One thing I noticed during testing: if I call updateName("Alice") twice in a row with the same string, StateFlow is smart enough not to notify the observers again. It does a check (essentially if (oldValue == newValue) return) before emitting. This is a huge win over the manual listener pattern, where you'd either have to write that check yourself or suffer through unnecessary UI re-renders.
By using StateFlow, we've turned the Observer pattern from a structural chore into a data stream. We have a single source of truth, automatic "last-known-value" delivery, and built-in deduplication.
📋 Practical Task
Exercise: Implementing a System Health Monitor
You need to build a SystemHealthMonitor that tracks the CPU temperature of a server. Multiple dashboard components (a TemperatureGauge and a SafetyAlertSystem) need to observe this temperature in real-time.
Requirements:
- Create a
SystemHealthMonitorclass that uses aMutableStateFlowto track anIntrepresenting the temperature. - The initial temperature should be
40. - Expose the temperature as a read-only
StateFlow. - Implement a function
updateTemperature(newTemp: Int)to update the state. - In your
mainfunction (within arunBlockingscope), launch two separate coroutines to collect the temperature. One should print "Gauge: [temp]°C" and the other should print "Alert: High Temp!" only if the temperature exceeds 80°C. - Simulate a temperature spike by updating the temperature to 50, then 90, then 90 again (to verify that
StateFlowdoesn't emit the second 90).
There are no comments for now.