Android Tutorial: Learning to use the EventBus Pattern

Michael Ganchas
The Startup
Published in
5 min readOct 21, 2020
Image from https://www.manypixels.co/gallery/

Hi all!

Today I’d like to share with you how we can make use of the EventBus pattern, a subset of the Publish-Subscribe pattern, for decoupled communication between different components.

When it might be useful

This pattern can be a good approach when you want to send (publish) data from ComponentA and receive and react to it (subscribe) from ComponentB.

The best analogy I could think of for this was of a ticket machine providing you your ticket number - after you’ve ordered that delicious meal at your local fast-food chain restaurant -, and then presenting on a screen what ticket number holder can go and collect their meal.

Details

We’re going to use Kotlin for this tutorial.

For this exercise, we’re going to create three different components, each with their own responsibility:

  • TicketMachine: Responsible for giving you your ticket number after you’ve ordered your meal and for informing anyone who’s interested (or no one) in knowing that there’s a new order.
  • TicketScreen: Responsible for informing what ticket number holder can collect its meal.
  • HappyCustomer A and B: The customers who’ll order their meal. They will receive a unique ticket number from TicketMachine and wait for TicketScreen to present their ticket number.

We’ll use this framework to implement it in Android. Add the following line to your app/build.gradle file:

// Event bus
implementation 'org.greenrobot:eventbus:3.2.0'

Models

For publishing and subscribing, the framework uses the type of object to separate what is being published and to whom it must deliver. So, let’s start out by defining our models.

Meal:

When a new customer selects the type of meal it wants, it just knows the burger name it selected, so a possible implementation would be as the following data class:

data class Meal(var burgerName : String)Publisher: CustomerA...Z
Subscriber(s): TicketMachine

DetailedMeal:

After receiving the meal details, the TicketMachine will be responsible for assigning it a unique ticket number, hence creating the following type:

data class DetailedMeal(val meal : Meal, 
val ticket : Ticket)
Publisher: TicketMachine
Subscriber(s): TicketScreen

FinishedMeal:

Once the meal has been prepared and is ready for delivery, the TicketScreen will need to inform the ticket number holder that it can go and grab its fabulous lunch. For internal analytics reasons, we will need to register when each meal is delivered, so the following might suit our goal:

data class FinishedMeal(val meal: DetailedMeal, 
val deliveredTime : Date)
Publisher: TicketScreen
Subscriber(s): CustomerA...Z

The implementation

For simplicity reasons, each customer has a unique button that will trigger the ordering a meal intent. The TicketMachine and TicketScreen will be singleton objects, with their start/stop listeners attached to the activity’s lifecycle.

MainActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btnCustomerA.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Single Happy Burger"
)
)
}

btnCustomerB.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Double Happy Burger"
)
)
}
}
}

We added the ordering events for each available customer. Now, we need to register ourselves to start paying attention to whatever events we might have interest in and unregister when we’re out of the store. At the same time, we’ll tell the TicketMachine and the TicketScreen to either start or stop working:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btnCustomerA.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Single Happy Burger"
)
)
}

btnCustomerB.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Double Happy Burger"
)
)
}
}

override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
TicketMachine.onStart()
TicketScreen.onStart()
}

override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
TicketMachine.onStop()
TicketScreen.onStop()
}

}

The only thing left is to say we need to know when a meal is finished:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btnCustomerA.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Single Happy Burger"
)
)
}

btnCustomerB.setOnClickListener {
EventBus.getDefault().post(
Meal(
"Double Happy Burger"
)
)
}
}

override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
TicketMachine.onStart()
TicketScreen.onStart()
}

override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
TicketMachine.onStop()
TicketScreen.onStop()
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onOrderReady(meal: FinishedMeal) {
Toast.makeText(this, "Meal ready for ticket number: ${meal.meal.ticket.number} at ${meal.deliveredTime}", Toast.LENGTH_LONG).show()
}

}

Note: We subscribe to the MainThread because we’re going to show a toast, which is handled in it.

Note 2: Read more on string interpolation here.

TicketMachine

Has a property with the current ticket number and is responsible for generating the next one.

object TicketMachine {
var currentTicket = 0
private set


private fun getNextTicket() {
currentTicket++
}
}

Other than that, it needs to know when a new meal has been ordered, and attach to it its unique ticket number and the time it was printed.

object TicketMachine {
var currentTicket = 0
private set

fun
onStart() {
EventBus.getDefault().register(this)
}

fun onStop() {
EventBus.getDefault().unregister(this)
}

private fun getNextTicket() {
currentTicket++
}

@Subscribe
fun onNewCustomer(meal: Meal) {
getNextTicket()
val detailedMeal = attachTicketToMeal(meal)
askToPrepareMeal(detailedMeal)
}

private fun attachTicketToMeal(meal: Meal): DetailedMeal {
return DetailedMeal(
meal,
Ticket(
currentTicket,
Calendar.getInstance().time
)
)
}

private fun askToPrepareMeal(meal: DetailedMeal) {
EventBus.getDefault().post(meal)
}

}

Note: Since there’s no requirement to handle any UI parts, the Subscribe annotation is in its default state.

TicketScreen

For this exercise, we’re going to use a CountDownTimer to simulate the time a meal needs to be finished. To register our orders, we’re going to use a simple LinkedList, which is a type of Queue, so that it is FIFO (first-in-first-out).

object TicketScreen
{
private const val AVERAGE_MEAL_TIME: Long = 10 * 1000 // 10 seconds

private val orders = LinkedList<DetailedMeal>()
private var timer : CountDownTimer? = null

fun
onStart() {
setTimerConditions()
timer?.start()
EventBus.getDefault().register(this)
}

fun onStop() {
EventBus.getDefault().unregister(this)
timer?.cancel()
}

private fun setTimerConditions()
{
timer = object: CountDownTimer(
Long.MAX_VALUE,
AVERAGE_MEAL_TIME
) {
override fun onTick(millisUntilFinished: Long)
{
val order = orders.poll() ?: return
...

}
override fun onFinish() { /* will never happen */ }
}
}
}

So now we need to say that we want to know when a meal has been attached to a ticket number, so that we can start cooking it.

object TicketScreen
{
private const val AVERAGE_MEAL_TIME: Long = 10 * 1000 // 10 seconds

private val orders = LinkedList<DetailedMeal>()
private var timer : CountDownTimer? = null

fun
onStart() {
setTimerConditions()
timer?.start()
EventBus.getDefault().register(this)
}

fun onStop() {
EventBus.getDefault().unregister(this)
timer?.cancel()
}

private fun setTimerConditions()
{
timer = object: CountDownTimer(
Long.MAX_VALUE,
AVERAGE_MEAL_TIME
) {
override fun onTick(millisUntilFinished: Long)
{
val order = orders.poll() ?: return
...

}
override fun onFinish() { /* will never happen */ }
}
}

@Subscribe
fun onNewMeal(meal: DetailedMeal) {
orders.add(meal)
}

}

After that, the only thing missing is to say that the next meal in the queue is ready, so that the correct customer can come and collect it.

object TicketScreen
{
private const val AVERAGE_MEAL_TIME: Long = 10 * 1000 // 10 seconds

private val
orders = LinkedList<DetailedMeal>()
private var timer : CountDownTimer? = null

fun
onStart() {
setTimerConditions()
timer?.start()
EventBus.getDefault().register(this)
}

fun onStop() {
EventBus.getDefault().unregister(this)
timer?.cancel()
}

private fun setTimerConditions()
{
timer = object: CountDownTimer(
Long.MAX_VALUE,
AVERAGE_MEAL_TIME
) {
override fun onTick(millisUntilFinished: Long)
{
val order = orders.poll() ?: return
callCustomer(order)
}
override fun onFinish() { /* will never happen */ }
}
}

private fun callCustomer(meal : DetailedMeal) {
val finishedMeal = FinishedMeal(
meal,
Calendar.getInstance().time
)
EventBus.getDefault().post(finishedMeal)
}

@Subscribe
fun onNewMeal(meal: DetailedMeal) {
orders.add(meal)
}
}

That’s it for today. Feel free to ask any questions or add suggestions if you’d like!

Cheers!

--

--