Simple Event Bus in Android using coroutines and flows

Simple Event Bus in Android using coroutines and flows

In this post I will describe a simple event bus pattern in Android that is implemented using coroutines and flows. Thanks to Kotlin, coroutines, and flows, this is a simple approach that requires very little code.

What is an Event Bus?

Event bus is a pattern that can be implemented in any programming language. An Event Bus is simply a class that implements this pattern.

In Android, there are libraries that have named themselves "EventBus," but it is not a library by default, it is a pattern that can be implemented quite easily without the use of any libraries.

The event bus approach and pattern is a tool that can be leveraged in an event-driven application. Event-driven applications are reactive and react to events as they occur.

One of the benefits of using the event bus pattern is to simplify your application architecture and decouple components that may need to know about each others' data. So, rather than injecting components into each other—which is not very scalable as your app grows—components can emit events and other components can subscribe to these events. This means that your components can instead depend on a shared EventBus class, to which they can publish and subscribe events, rather than on the components themselves that would be required to get this information.

When should I use an Event Bus?

You can use an Event Bus when you need to notify the rest of the app that something has happened, without worrying about all of the dependencies, the hierarchy of your app, or the lifecycles.

Some examples of events that may be appropriate to use the event bus pattern for:

  • internet connection is lost or regained
  • the user logs out of the app
  • a long-running task has completed in the background and other components may want to be notified of this task completion
  • data is updated in the background and other components may want to know when this data is updated so that they may re-fetch it

You can use events for pretty much anything but they may not make sense for everything.

When should I NOT use an Event Bus?

An Event Bus is not a one-stop solution—it is a tool that is useful in some cases but may not make sense in other cases.

If you have a view that, on mount, fetches data, and you want the view (fragment or activity) to be aware of this data once it's fetched, an event bus in Android is not the best approach for this. For this, I would instead recommend using MVVM (Model - View - View Model) architecture, and implementing a LiveData observable. With LiveData, you can observe changes to this data, so that this data (once changed) can be updated in the view.

The event bus pattern is not meant to replace Live Data and observables, but rather complement it. You can use both at the same time, if necessary. It is very valid to have both an event bus and live data observables in an application as they have different use cases and value.

A simple example EventBus implementation

EventBus class

The following example EventBus class uses Kotlin coroutine flows to provide events to subscribers. You can learn more about coroutines in Android here. Here is the example EventBus class:

import android.util.Log
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

class EventBus {
    private val _events = MutableSharedFlow<AppEvent>()
    val events = _events.asSharedFlow()

    suspend fun emitEvent(event: AppEvent) {
        Log.d(TAG, "Emitting event = $event")
        _events.emit(event)
    }

    companion object {
        private const val TAG = "EventBus"
    }
}

In the above code sample, AppEvent is a simple enum. This approach would keep your event bus simple. You may want to instead use a sealed class so you can pass data alongside this event, though I would probably limit the complexity of the event bus by just emitting simple events and having subscribers do what they want with that data. This may or may not be the simplest or most practical approach in your application, so consider your app's needs.

It is important to have a single instance of the event bus so that consumers are publishing and subscribing to the same event bus. In most cases, this EventBus would be provided as a singleton using your dependency injection framework of choice, like Koin or Dagger Hilt, both of which are out of scope for this article.

Publishing events

Publishers who want to publish events would call the emitEvent method with the desired event on the EventBus instance:

eventBus.emitEvent(AppEvent.CONNECTION_RECONNECTED)

Subscribing to events

Subscribers who want to listen to events would collect the emitted events on the flow. This implementation could look something like this:

eventBus.events
    .filter { it == AppEvent.CONNECTION_RECONNECTED }
    .collectLatest { handleReconnection() }

Caveats with the Event Bus approach

While an event bus is a useful tool, like anything, it can be abused. An application's state can quickly get out of hand if every component is using the event bus for everything. It could get to a point where events are being fired unexpectedly, causing unexpected side effects in your app.

Due to this, it may make sense to only use such an approach for events that need to be global in nature (e.g. network connection is dropped or connected, the user logs out, etc.), and stick to conventional MVVM architecture for most of your app's data.

Summary

In summary, the event bus pattern is a useful approach to reactive programming but is not a solution for everything. In most cases, LiveData observables may solve your problem, but in some cases you may want to leverage an event bus to react to events that are more global in nature as they occur.