Keep listening or do your job and finish
Tags: kotlin, api
Published: 2024-06-30 09:58:09

People talk a lot about so-called design patterns (usually referencing those made in the [famous book](https://en.wikipedia.org/wiki/Design_Patterns) for C++).  Though those patterns themselves are not universal, they change from language to language and their usage strongly depends on the specific task at hand. However, there are several among them that are more frequently used. One of those is the listener/observer pattern, which implies that an object is allowed to attach a listener object to it to observe its changes or other events.

In Kotlin syntax a general API for using a listener looks like this:

```kotlin
interface Observable {
  
    fun addListener(owner: Any?, listener: (Event) -> Unit)
    
    fun removeListener(owner: Any?)
}
```

The first method of this interface is quite obvious and familiar. `listener` is the callback which is used when something interesting happens in the `Observable`. The `Event` could be different and even could have several arguments instead of one. The thing people tend to forget about is the second argument: `owner`. It becomes important when we remember that listeners need not only to be added but removed as well. When you remove a listener, you need to specify which listener to remove. So, you either need to return a removal handler from an `addListener` method or provide an identity to the removed object. Sometimes people try to use the listener itself to provide listener identity like this: `fun removeListener(listener: (Event) -> Unit)`. But doing this is most probably a mistake because lambda functions identity is a tricky thing and at some point, it could produce unexpected results. Furthermore, one needs to store this function somewhere in case it needs to be removed. It is much better to use the owner of the listener (an object, that has an identity) as a handle. As an additional benefit, one could remove all listeners from the same handle or even use a `null` handle to remove all listeners.

Even with the handle identity problem solved, there is a problem of "hanging" listeners. In a dynamic structure, it is quite easy to forget to "detach" the listener that points to removed objects. It leads to a memory leak because the referenced object still exists in the listener so it could not be collected by GC.

The way to solve this problem is inspired by Kotlin [`SharedFlow`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/). It attaches a listener on each subscription and removes said listener automatically when the subscription flow is canceled or collected. This way, you do not need to think about removing the listener manually. You just stop listening.

Let us see, how we can do that in a "flowless" environment:

```kotlin
interface AutoDetachObservable {
    fun listen(
        scope: CoroutineScope, 
        listener: (Event) -> Unit
    ): Job
}
```

The `scope` provides a `CoroutineScope` in which the listener runs. When it is canceled, the listener (and all other listeners in the scope) is automatically detached. And the resulting `Job` could be canceled manually thus canceling only this subscription. This way the construct has not one, but two levels of control for subscription cancelation.

The implementation of this mechanism could look like this:

```kotlin
fun listen(
    scope: CoroutineScope, 
    listener: (Event) -> Unit
): Job{
    val id = generateUniqueId()
    val job = scope.launch{ listenerRegistry[id] = listener }
    job.invokeOnCompletion{ listenerRegistry.remove(id) }
    return job
}
```

This way the removal of the listener will be triggered both on scope cancelation and on `Job` cancelation. The `id` is a simple internal implementation detail that ensures that internal listeners can be removed. It is attached to a specific job, so it needs not to be exposed to the external user.

An additional benefit of said approach is that there is a `CoroutineScope` provided, so with some additional changes, it is possible to make `listener` a suspended function and run it in the context of a listener, not in the target object. Making listeners suspended is quite useful in concurrent systems to avoid data races for events.

---
After I wrote the post, I found out that I've already written a related one two years ago: https://dev.to/altavir/to-flow-or-not-to-flow-message-subscription-in-kotlin-57ea.

---

The cover image is taken from ["Большой Ух"](https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D0%BB%D1%8C%D1%88%D0%BE%D0%B9_%D0%A3%D1%85) cartoon.