Event-based systems on Android — feat. RxJava and Kotlin

It was the best of times. It was the worst of times. So begins our tale of two classes.

Sending out an S.O.S…

Imagine a software system that is divided into two components: it is common parlance to design software systems to be modular; that is, a given feature should contain within itself the full specification of what it needs to operate. It may have external contingencies, but its internal logic is agnostic as to what the sources of those contingencies are.

In such a system, it is sometimes necessary to cross some sort of boundary, whether a synthetically designed boundary within the software system itself, or an actual hardware boundary where some conversion of information and energy needs to happen, such as when reaching out to a server on a computer somewhere.

In this article, I want to focus on boundaries within a given software system. In Android, there are many such boundaries that we are forced to deal with on a constant basis. For instance, there are the Activity and Fragment interfaces. Information does not readily pass between Activity and Fragment but through a particular transfer mechanism called a Bundle. Bundles are the interface provided for sending information across the Activity/Fragment divide, so that it will become available at initialization time.

Information transfer is often more like a private road than an open highway. We need to design systems which support stable inter-module communication, despite such channels being restricted. One way to do this is by means of events. RxJava provides natural mechanisms enabling communication across architectural boundaries by means of “observable” event streams.

So join me in this series of “events”, as we engage in this fun “activity” and look at some awesome code “fragments”…

Creating the events

Kotlin provides a neat feature known as a Sealed Class. And when I say neat, I mean really neat. Sealed classes are like enums. They let you fully specify possible instances of a class. Then you can safely iterate over the possibilities without fearing that you missed any.

We want a sealed class that will enumerate the types of events we expect to use. To start with, let’s say we have two events, Show and Dismiss, which will — unsurprisingly — be responsible for showing and dismissing the dialog, respectively:

// AppEvent.ktsealed class AppEvent {
object Show: AppEvent()
object Dismiss: AppEvent()
}

Since our events don’t have constructors, we can use object syntax to declare a single instance representing that event. By doing this, references to AppEvent.Show will always point to the same instance (***we can therefore use === to perform referential equality checks).

Bringing in RxJava

How about with RxJava? We should just need one library to start using RxJava on Android:

// rxAndroid
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"

We will accomplish this through the use of an event stream called a Flowable, and it’s foil the PublishProcessor. The former is what we will subscribe to to listen to events; the latter we will use to publish events on that same stream. As we will see, it is possible to directly cast a PublishProcessor to a Flowable. That way, we know for sure we are publishing and subscribing to the same event stream. Let’s add on to AppEvent.kt:

val appEventProcessor: PublishProcessor<AppEvent> = PublishProcessor.create()
val appEventFlowable = appEventProcessor as Flowable<AppEvent>

In Kotlin, you can do naked variable declarations like this from any file. These declarations will effectively function as global variables, accessible from anywhere within your program.

***Important note: it is also possible to do this with Dagger 2. Actually my initial intention had been to do it that way, providing the PublishProcessor and Flowable from a module and injecting them. That is probably the more elegant and extensible approach. But it is a lot easier to explain this way.

Subscribe to AppEvents from Activity

I usually set up a subscription with a method on the Activity, like so:

// MainActivity.ktprivate fun createAppEventsSubscription(): Disposable =
appEventFlowable
.doOnNext {
when (it) {
AppEvent.Show -> { /* do something */ }
AppEvent.Dismiss -> { /* do something */ }
}
}
.subscribe()

***Edit
As noted in the comments by Areeb Jamal, I have fallen victim to the pitfall of “using side effects.” That is, I should perform the required updates in subscribe, and also pass an error handler, rather than using doOnNext with a raw subscribe() tacked on the end, leaving myself vulnerable to unexpected errors. I’m leaving it this way because I think it’s more readable, but readers should be advised that this is technically an anti-pattern and should generally be avoided.
***

Notice that we are not required to put an else clause in the when block. That is because the compiler is able to infer we have exhausted all possibilities by using a Sealed Class.

I add this subscription to a CompositeDisposable in my Activity’s onCreate() method, and make sure to clean up my subscriptions in onStop(). This is basically to disable subscriptions when the Activity is not within the “visible” section of its lifecycle:

// MainActivity.ktvar compositeDisposable = CompositeDisposable()

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

compositeDisposable.add(createAppEventsSubscription())
}
override fun onStop() {
super.onStop()
compositeDisposable.clear()
compositeDisposable = CompositeDisposable()
}

The Fragment

Here’s our layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/dismiss_button"
android:text="DISMISS"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</android.support.constraint.ConstraintLayout>

As you can see, it’s literally just a centered button that says DISMISS on it.

Now the Fragment:

// MainFragment.ktclass MainFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_main, container).apply {
dismiss_button.setOnClickListener {
appEventProcessor.onNext(AppEvent.Dismiss)
}
}
}

We do a lot of cool Kotlin stuff here, utilizing apply to return the receiver after inflating the view, as well as synthetic view references to assign a click listener. More importantly, we trigger AppEvent.Dismiss on our global processor whenever the button is clicked. Notice the Fragment is not handling any other logic itself. That part will be managed by the subscriber of the event, our Activity.

Completing the circuit

// MainActivity.ktvar mainFragment: MainFragment? = nullprivate fun createAppEventsSubscription(): Disposable =
appEventFlowable
.doOnNext { Log.d("AppEvents", "$it") }
.doOnNext {
when (it) {
AppEvent.Show -> {
if (mainFragment == null) {
mainFragment = MainFragment().apply {
supportFragmentManager
.beginTransaction()
.add(android.R.id.content, this)
.commit()
}
}
}
AppEvent.Dismiss -> {
mainFragment?.let {
supportFragmentManager
.beginTransaction()
.remove(it)
.commit()
}

mainFragment = null
}
}
}
.subscribe()

Let’s also automatically trigger a Show event in OnStart():

// MainActivity.ktoverride fun onStart() {
super.onStart()
appEventProcessor.onNext(AppEvent.Show)
}

And that’s it! Running the app should show you sequence of two states: one when the app loads, showing the Fragment, and one revealing the original Activity after clicking the dismiss button:

So that was a whirlwind toward of event-based architecture as it relates to Android. This is one angle, using RxJava, which I highly prefer as it makes for a very understandable and extensible architecture. It allows you to respond to changes right within the components to whom those changes affect, rather than delegating responsibilities all over the place in a tangled mess.

If you read this far, thanks so much, hope you found something interesting and of value. See you next time!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store