Solution: Correct EventListenerRegistry
The first problem is that the handler
variable in the EventListener
class is not cleared when the listener is canceled. It might be a heavy object, which is why we should clear it. We can do this by setting handler
to null
in the cancel
method.
The second, smaller problem is that the listeners
variable in EventListenerRegistry
does not eliminate canceled listeners. We can do this by removing canceled listeners from the set when listeners are invoked.
class EventListenerRegistry<E> {
private var listeners = ConcurrentHashMap
.newKeySet<EventListener<E>>()
private val lock = Any()
fun addEventListener(
event: E,
handler: () -> Unit
): EventListener<E> = synchronized(lock) {
val listener = EventListener(event, handler)
listeners += listener
listener
}
fun invokeListeners(event: E) {
val eventListeners = listeners
.filter { it.event == event }
for (listener in eventListeners) {
if (listener.isActive) {
listener.handleEvent()
} else {
listeners.remove(listener)
}
}
}
}
class EventListener<E>(
val event: E,
handler: () -> Unit,
) {
private var handler: (() -> Unit)? = handler
val isActive: Boolean get() = handler != null
fun handleEvent() {
handler?.invoke()
}
fun cancel() {
handler = null
}
}
This problem can also be solved by removing canceled listeners from a set immediately after canceling them, but this solution would require event listeners to have access to the repository, which might be problematic in some cases.
import org.junit.Test
import java.util.concurrent.ConcurrentHashMap
import kotlin.test.assertEquals
class EventListenerRegistry<E> {
private var listeners = ConcurrentHashMap
.newKeySet<EventListener<E>>()
private val lock = Any()
fun addEventListener(
event: E,
handler: () -> Unit
): EventListener<E> = synchronized(lock) {
val listener = EventListener(event, handler)
listeners += listener
listener
}
fun invokeListeners(event: E) {
val eventListeners = listeners
.filter { it.event == event }
for (listener in eventListeners) {
if (listener.isActive) {
listener.handleEvent()
} else {
listeners.remove(listener)
}
}
}
}
class EventListener<E>(
val event: E,
handler: () -> Unit,
) {
private var handler: (() -> Unit)? = handler
val isActive: Boolean get() = handler != null
fun handleEvent() {
handler?.invoke()
}
fun cancel() {
handler = null
}
}
enum class Event { A, B, C }
class EventListenerRegistryTest {
@Test
fun `should invoke proper handlers`() {
val eventListenerRepository = EventListenerRegistry<Event>()
var a = 0
var b = 0
var c = 0
eventListenerRepository.addEventListener(Event.A) { a++ }
eventListenerRepository.addEventListener(Event.B) { b++ }
eventListenerRepository.addEventListener(Event.C) { c++ }
assertEquals(0, a)
assertEquals(0, b)
assertEquals(0, c)
eventListenerRepository.invokeListeners(Event.A)
assertEquals(1, a)
assertEquals(0, b)
assertEquals(0, c)
eventListenerRepository.invokeListeners(Event.B)
eventListenerRepository.invokeListeners(Event.B)
assertEquals(1, a)
assertEquals(2, b)
assertEquals(0, c)
eventListenerRepository.invokeListeners(Event.C)
eventListenerRepository.invokeListeners(Event.C)
eventListenerRepository.invokeListeners(Event.C)
assertEquals(1, a)
assertEquals(2, b)
assertEquals(3, c)
}
@Test
fun `should allow setting more than one handler for an event`() {
val eventListenerRepository = EventListenerRegistry<Event>()
var a = 0
var b = 0
var c = 0
eventListenerRepository.addEventListener(Event.A) { a++ }
eventListenerRepository.addEventListener(Event.A) { b++ }
eventListenerRepository.addEventListener(Event.A) { c++ }
eventListenerRepository.invokeListeners(Event.A)
assertEquals(1, a)
assertEquals(1, b)
assertEquals(1, c)
}
@Test
fun `should allow listener cancelation`() {
val eventListenerRepository = EventListenerRegistry<Event>()
var a = 0
val listener = eventListenerRepository.addEventListener(Event.A) { a++ }
listener.cancel()
eventListenerRepository.invokeListeners(Event.A)
assertEquals(0, a)
}
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.