Young Devs Bin
๐Ÿงฉ Design Pattern

Observer Pattern: Notifying Many Without Knowing Who's Listening

ยท2 min readยท#design-pattern#observer#kotlin#android#ood

Second pattern this week: Observer. The setup was a lane control system โ€” sensor detects a lane status change, and multiple components (speed controller, warning system, logger) all need to react. The naive version calls each component directly from the subject, which hard-codes every dependency. Observer breaks that coupling.

  • The key insight is that the subject shouldn't care who reacts or how โ€” it just maintains a list and notifies everyone; each observer decides what to do with the event
interface LaneObserver {
    fun onLaneStatusChanged(status: LaneStatus)
}
 
class LaneController {
    private val observers = mutableListOf<LaneObserver>()
    private var currentStatus: LaneStatus = LaneStatus.CLEAR
 
    fun subscribe(observer: LaneObserver) = observers.add(observer)
    fun unsubscribe(observer: LaneObserver) = observers.remove(observer)
 
    fun updateLane(newStatus: LaneStatus) {
        if (newStatus == currentStatus) return  // skip duplicate notifications
        currentStatus = newStatus
        observers.forEach { it.onLaneStatusChanged(currentStatus) }
    }
}
  • Adding a new component that reacts to lane changes is one step: implement LaneObserver and subscribe โ€” LaneController never changes, which is Open/Closed in action
  • Deduplication matters: checking if (newStatus == currentStatus) return before notifying prevents observers from firing on a no-op update
  • Runtime subscribe/unsubscribe means observers can come and go โ€” useful when a component is conditionally active (e.g., warning system is disabled in certain modes)
class SpeedController : LaneObserver {
    override fun onLaneStatusChanged(status: LaneStatus) {
        val speed = when (status) {
            LaneStatus.CLEAR -> "Full speed"
            LaneStatus.MERGING -> "Reduce to 30mph"
            LaneStatus.BLOCKED, LaneStatus.CONSTRUCTION -> "Stop"
        }
        println("[SpeedController] $speed")
    }
}
  • When sensor data arrives asynchronously, the sync Observer model isn't enough โ€” the upgrade path is replacing updateLane() with a MutableStateFlow so the ViewModel can collect lane updates on Dispatchers.IO and expose them lifecycle-safely with collectAsStateWithLifecycle
  • Observer pattern and StateFlow solve the same core problem at different layers: Observer for class-to-class notification in pure Kotlin, StateFlow for ViewModel-to-UI notification in Android
  • Testing is straightforward: mock the observer, call updateLane(), and verify that onLaneStatusChanged() was called with the right status โ€” MockK handles this in two lines