Behavioral Design Pattern
Kotlin Design Patterns: Observer Explained
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:
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:
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.
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:
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)