Reputation: 94
My goal is to implement very simple event bus in Kotlin without any 3rd party libraries. I accomplished that with the code below.
class EventListener<T>(
val owner: Any,
val event: Class<T>,
val callback: (T) -> Unit
)
interface IEventBus {
fun <T> subscribe(owner: Any, event: Class<T>, callback: (T) -> Unit)
fun unsubscribe(owner: Any)
fun <T> push(event: T)
}
class EventBus : IEventBus {
private val _listeners = mutableListOf<EventListener<*>>()
override fun <T> subscribe(owner: Any, event: Class<T>, callback: (T) -> Unit) {
val listener = EventListener(owner, event, callback)
_listeners.add(listener)
}
override fun unsubscribe(owner: Any) {
_listeners.removeAll {
it.owner == owner
}
}
override fun <T> push(event: T) {
_listeners.forEach { listener ->
try {
val l = listener as EventListener<T> // is always a success
l.callback(event) // throws an exception if can't handle the event
} catch (ex: Exception) { }
}
}
}
And then the usage would be like this:
// register listener
bus.subscribe(this, String::class.java) {
print(it)
}
// push an event (from somewhere else in the project)
bus.push("Hello world!")
It works and is completely usable, however I am not satisfied with it... Casting listener as EventListener will always return something and then if l.callback(event) can't handle the event type it will throw an exception. So if there are many listeners subscribed then it will generate many unwanted exceptions that will just be ignored.
I would prefer doing some kind of check first, like:
if (listener is EventListener<T>)
listener.callback(event)
But I found out that JVM loses information about the generic types after the compilation. I also found out that it can be bypassed using kotlin's inline and reified, however those can not be used on a method that comes from an interface...
So my question is do you know any more elegant way to handle such generic problem?
Upvotes: 5
Views: 2367
Reputation: 2900
The documentation of kotlinx.coroutines.flow.SharedFlow contains a simple example:
SharedFlow is useful for broadcasting events that happen inside an application to subscribers that can come and go. For example, the following class encapsulates an event bus that distributes events to all subscribers in a rendezvous manner, suspending until all subscribers process each event:
class EventBus {
private val _events = MutableSharedFlow<Event>() // private mutable shared flow
val events = _events.asSharedFlow() // publicly exposed as read-only shared flow
suspend fun produceEvent(event: Event) {
_events.emit(event) // suspends until all subscribers receive it
}
}
Besides, the corresponding Android documentation is useful, too.
Upvotes: 0
Reputation: 16214
Since you are already exposing the class of the event (EventListener#event
) you can use isInstance()
to check if the class is assignment-compatible with the instance of your event.
So, instead of:
if (listener is EventListener<T>)
listener.callback(event)
you can do:
if (listener.event.isInstance(event)) {
// The cast is safe since you checked if the event can be received by the listener.
(listener as EventListener<T>).callback(event)
}
Primitives
If you want to support T
with primitive types too, you can change Class<T>
to KClass<T>
or check the instance manually on each primitive type (e.g. event is Int
, event is Long
).
Upvotes: 2