Kotlin equivalent of C# events

Viewed 1587

I am new in Kotlin and was trying to find any equivalent in Kotlin for C# events. I couldn't find any examples so I suppose there aren't any events in Kotlin. Am I right? And also how this functionality in C# can be done in Kotlin?

class A
{
    public event Action<int> NormalEvent;
    public static event Action<int> StaticEvent;

    public void FunA(int x)
    {
        NormalEvent?.Invoke(x);
        StaticEvent?.Invoke(x);

    }
}

class B
{
    private readonly A aInstance;

    public void FunBNormal(int x)
    {
        Console.WriteLine($"Normal event {x}");
    }

    public void FunBStatic(int x)
    {
        Console.WriteLine($"Static event {x}");
    }

    public B(A a)
    {
        aInstance = a;
        aInstance.NormalEvent += FunBNormal;
        A.StaticEvent += FunBStatic;
    }
}
4 Answers

There isn't a built-in language construct for this, but it would be rather trivial to create a class for it. Maybe it would take a little more work to make it thread-safe and avoid leaking listeners. It depends on your needs.

class Event<T> {
    private val observers = mutableSetOf<(T) -> Unit>()

    operator fun plusAssign(observer: (T) -> Unit) {
        observers.add(observer)
    }

    operator fun minusAssign(observer: (T) -> Unit) {
        observers.remove(observer)
    }

    operator fun invoke(value: T) {
        for (observer in observers)
            observer(value)
    }
}
class A {
    companion object {
        val staticEvent = Event<Int>()
    }
    val normalEvent = Event<Int>()

    fun funA(x: Int) {
        normalEvent(x)
        staticEvent(x)
    }
}

class B(private val aInstance: A) {
    init {
        aInstance.normalEvent += ::funBNormal
        A.staticEvent += ::funBStatic
    }

    fun funBNormal(x: Int) {
        println("Normal event $x")
    }


    fun funBStatic(x: Int) {
        println("Static event $x")
    }
}

You can use Signals for this. It uses a proxy magic to turn an interface into a Signal object. It holds on listeners and fire events.

fun interface Chat{
    fun onNewMessage(s:String)    
}

class Foo{
    val chatSignal = Signals.signal(Chat::class)
    
    fun bar(){
        chatSignal.addListener { s-> Log.d("chat", s) } // logs all the messaged to Logcat
    }
}

class Foo2{
    val chatSignal = Signals.signal(Chat::class)
    
    fun bar2(){
        chatSignal.dispatcher.onNewMessage("Hello from Foo2") // dispatches "Hello from Foo2" message to all the listeners
    }
}

The equivalent for static and normal events in your example will be

  • Signal.signal() - creates a singleton Signal object
  • Signal.localSignal() - creates a new instance of a Signal object

Disclaimer: I am the author of Signals.

If you want something native you can try using Delegates objects

Example (in my example I notify a simple string but you can do it with any type of object)

import kotlin.properties.Delegates

class A {

     private  var myEvent: String by Delegates.observable("") {_, oldValue, newValue ->
        onMyEventChanged?.invoke(oldValue,newValue)
    }
    var onMyEventChanged: ((String, String) -> Unit)? = null

    //somewhere in this class you set the variable that throws the event
    //calling this method
    funA(x: String) {
        myEvent = x
    }

}

class B(private val aInstance: A) {
    init {
    //every time the private variable myEvent is set in class A, the 
     //onMyEventChanged event is raised in class B
        aInstance.onMyEventChanged = {oldValue, newValue ->
          funB(newValue)
        }
    }

    fun funB(x: String) {
        println("My event : $x")
    }

}


With the Delegates you have the advantage of also checking the old value that is passed to you but you have the disadvantage that you cannot unsubscribe the event

I feel like the simplest solution to this is with Kotlin Flows.

class A {
    private val _event = MutableSharedFlow<Unit>()
    val event get() = _event.asSharedFlow()
}

class B {
    private var subscription: Job? = null

    suspend fun subscribeToEvents(a: A) {
        subscription?.cancel()
        subscription = launch {
            a.event.onEach {
                // Event has been fired...
            }.collect()
        }
    }

    fun cancelSubscriptions() {
        subscription?.cancel()
    }
}

class C {
    private var subscription: Job? = null

    suspend fun subscribeToEvents(a: A) {
        subscription?.cancel()
        subscription = launch {
            a.event.onEach(::handleEvent).collect()
        }
    }

    private suspend fun handleEvent(unit: Unit) {
        // Event has been fired...
    }

    fun cancelSubscriptions() {
        subscription?.cancel()
    }
}

This is thread safe and can be easily cleaned up at any time by either cancelling the Job, or cancelling the CoroutineScope the job was created in.

You could also simplify the process with an extension function that only works on Flow<Unit>.

suspend inline fun Flow<Unit>.subscribe(noinline handler: suspend (Unit) -> Unit): Job = coroutineScope {
    launch { this@subscribe.onEach(handler).collect() }
}

class D {
    private var subscription: Job? = null

    suspend fun subscribeToEvents(a: A) {
        subscription?.cancel()
        subscription = a.event.subscribe(::handleEvent)
    }

    private suspend fun handleEvent(unit: Unit) {
        // Event has been fired...
    }

    fun cancelSubscriptions() {
        subscription?.cancel()
    }
}

I also have a class I use all the time for dealing with job cancellations when subscribing to flows:

class SafeJob(initialValue: Job? = null) : ReadWriteProperty<Any?, Job?> {
    var value: Job? = initialValue; private set
    override fun getValue(thisRef: Any?, property: KProperty<*>): Job? = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Job?) = synchronized(this) {
        this.value?.apply { if (!isCancelled) cancel() }
        this.value = value
    }
}

class E {
    private var subscription: Job? by SafeJob()

    suspend fun subscribeToEvents(a: A) {
        subscription = a.event.subscribe(::handleEvent)
    }

    private suspend fun handleEvent(unit: Unit) {
        // Event has been fired...
    }

    fun cancelSubscriptions() {
        subscription = null
    }
}

The SafeJob will automatically cancel the previous job when ever the property's value is changed, and it can be simply set to null if you want to just clear it.

Related