Skip to Content
Course content

125: The Observer Pattern with Flow

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

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 SystemHealthMonitor class that uses a MutableStateFlow to track an Int representing 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 main function (within a runBlocking scope), 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 StateFlow doesn't emit the second 90).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.