Behavioral Design Pattern

Kotlin Design Patterns: Observer Explained

Michal Ankiersztajn
ProAndroidDev
Published in
3 min readApr 17, 2024

--

Purpose of the pattern

Creation of a subscriber-publisher mechanism to notify subscribers when a state changes. It’s helpful because this way, instead of having to check for state change periodically, we’re notified whenever the state changes.

What do we get from that?

  • Open/Closed, we can add subscribers without changes inside the publisher.
  • One-To-Many dependency without tight coupling.
  • State-based approach.
  • Efficient State observation.

Implementation

Typically, the implementation would look like this:

Typical Observer Class Diagram

And it’s an okay approach when using Java/C# or similar languages.

But we’re using Kotlin. We have a built-in Observer called Flow and Coroutine that can collect emitted values:

Kotlin Observer

Here Coroutine works like a Subscriber , Flow with Owner are replacement for Publisher now, we have separated Logic to Owner , publishing to Flow and collecting and canceling to Coroutine .

Depending on used Flow there might be a single collector or multiple collectors. It might emit the last cached value or only when something changes. You’re able to configure it to your needs.

Example

Your task is to write a shopping app. You must notify the client about the store equipment each time products change. The client should be able to stop getting notified.

Shop class diagram

Let’s start by creating Product and State as they’re the data holders:

data class Product(val name: String)

data class State(val products: List<Product> = emptyList())

Now, let’s create a Store :

class Store {
private val mutableState = MutableStateFlow(State())
val state = mutableState.asStateFlow()

fun addProduct(product: Product) {
mutableState.update { state ->
state.copy(products = state.products + product)
}
}

fun removeProduct(product: Product) {
mutableState.update { state ->
state.copy(products = state.products - product)
}
}
}

Note that we’re able to create Store without Client because it’s independent from it. Finally Client :

class Client(
private val store: Store,
) {
private val scope = CoroutineScope(SupervisorJob())
private var job: Job? = null

fun getNotified() {
if (job != null) return
scope.launch {
job = store.state.onEach { state ->
println(state.products.toString())
}.launchIn(this)
}
}

fun removeNotification() {
job?.cancel()
job = null
}
}

SupervisorJob cancels observing when the object is inactive. Job works until it’s canceled or SupervisorJob is canceled meaning we’ll collect the values as long as it’s active.

Here’s an example of how it works:

fun main() {
val store = Store()
val client1 = Client(store)
val client2 = Client(store)

val apple = Product("Apple")
store.addProduct(apple)

// Produced, because StateFlow emits last value when we start observing
client1.getNotified() // [Product(name=Apple)]
client2.getNotified() // [Product(name=Apple)]

store.removeProduct(apple) // It prints twice because we have 2 collectors
// []
// []

client1.removeNotification()

val banana = Product("banana")
store.addProduct(banana) // Now we only have 1 collector
// [Product(name=banana)]
}

Please note that the usage is very raw, and the tutorial is about Observer not Flows or Coroutines as they’re a vast topic, you should research if you haven’t used them before.

Thanks for reading! Please clap if you learned something, and follow me for more!

Learn more about design patterns:

Design Patterns In Kotlin

17 stories

Based on the book:

“Wzorce projektowe : elementy oprogramowania obiektowego wielokrotnego użytku” — Erich Gamma Autor; Janusz Jabłonowski (Translator); Grady Booch (Introduction author); Richard Helm (Author); Ralph Johnson (Author); John M Vlissides (Author)

--

--