Building a system of reactive components with Kotlin
This is the second part in a series of articles on Android architecture in Badoo. Check out the first one if you missed it:
What we want and how we’re gonna get there
In the first article of this series we looked at Features, the centerpiece of MVICore, which allow you to create reusable components of business logic. They can be as simple as having a single Reducer, with the option to go full-featured to handle asynchronous tasks, events, and much more.
Every Feature is observable, so you can subscribe to its state changes for automatic updates. Likewise, it can be subscribed to a source of inputs. This make sense, as since the adoption of Rx into our codebase we already have observables and subscriptions at many different levels.
But it’s exactly because of this growth in the number of reactive parts that now is a good time to reflect upon what we have and see if we can do any better.
We’re looking for answers to at least these three questions:
- What base building blocks should we choose if we are using more and more reactive components?
- How can we make subscription handling as simple as possible?
- Can we abstract away from handling the lifecycle/scope of those bindings? That is, can we separate the concern of binding components from the concern of managing subscriptions?
In this article we’ll look at some cornerstones (and benefits) of building your system using reactive components, and how Kotlin makes it so much simpler to do so.
Base building blocks
After a lot of earlier drafts and experiments, when we sat down to design and standardise our Features, we already understood that they would be reactive components. So, instead of jumping right in, we took a step back and tried to approach it from the outermost, high-level interfaces. The first building blocks we needed to define were the types of inputs and outputs.
Some factors we took into consideration:
- Let’s not reinvent the wheel — instead of creating new interfaces, let’s see what’s already out there.
- Since we are already using RxJava anyway, it would make sense to reuse its base interfaces.
- We need to keep those interfaces to a minimum.
So, finally we opted to use:
- ObservableSource<T> for outputs
- Consumer<T> for inputs
Why not Observable / Observer, you might ask? Observable is an abstract class that you need to extend while ObservableSource is an interface you can implement. And it already satisfies all our needs for an output that can be observed:
Similarly, even though Observer is an interface, and probably the first to come to mind, it has four methods: onSubscribe, onNext, onError, and onComplete. To keep things minimal, we opted to go with Consumer<T> instead. It has only one method for accepting a new element, and most of the time the other methods from Observer would either be irrelevant or handled differently (for example, we wanted to pass errors as part of the State and not via Exceptions, and certainly without terminating the stream).
So, now we have two interfaces, each with only one method. Neat! We can connect these by subscribing our Consumer<T> to the ObservableSource<T>. The latter only accepts instances of Observer<T>, but we can wrap it into an Observable<T> which has subscribe for Consumer<T>:
(Fortunately, the wrap doesn’t add any extra layer around the object if it’s already an actual Observable<T>.)
You might recall that the Feature component we saw in the first article has an input of type Wish (the Intent part of Model-View-Intent) and an output of type State, so it can be on either end of such a connection:
Connecting outputs and inputs already seems to be easy, but there’s actually an even better way. There’s no need to subscribe manually, nor do you need to handle subscription disposal — meet the Binder.
Binding on steroids
MVICore comes with a class called Binder, which provides an easy API around Rx subscriptions, and has some powerful abilities. Its responsibility is to:
- Create connections for you by subscribing an input to an output
- Dispose of those subscriptions when a given lifecycle ends (where lifecycle is an abstract concept and not tied to Android in any way)
- And, as an added bonus, it helps you add middlewares like logging or time travel debugging to any of these connections automatically
So, instead of manually subscribing, the above examples could be rewritten like this:
Nice and easy, thanks to Kotlin.
The above examples work because both the output and the input are of the same type. But what if they are different? With a Kotlin extension function from the library (“using”), we can have the transformation invoked automatically:
Notice how it reads like a sentence — one of the many things I love about Kotlin. But the Binder is more than just syntactic sugar. So what are those lifecycles I mentioned?
Creating an instance is as simple as:
This requires manual disposal — in this case it’s your responsibility to call binder.dispose() when the subscriptions need to be disposed of. The other way is to pass in a lifecycle instance in the constructor:
In this case you don’t need to worry about the subscriptions — they will be automatically disposed of when the lifecycle expires, with an additional extra: the lifecycle might repeat many times (think for example start / stop cycles on Android UI), in which case the Binder will re-subscribe your connections and dispose of them for you every time.
What is a lifecycle anyway?
If you’re an Android developer like me, when you see the word lifecycle, what first comes to mind is probably the Activity/Fragment lifecycle. And yes, the Binder can work with those and dispose of the subscriptions when their lifecycle ends.
But that’s just the beginning, because you are not tied to Android’s LifecycleOwner implementations in any way.
Binder has its own, more general interface for lifecycles. It’s basically a stream of BEGIN / END signals:
You can either supply this stream from an actual Observable (by mapping), or you can just use the ManualLifecycle class from the library for non-Rx environments (I’ll show you how in a minute).
How does this work with the Binder?
As soon as it receives a BEGIN element, the connections you told it to bind earlier are subscribed, and as soon as it receives END, those subscriptions are disposed of, simple as that. The fun thing is that you can rinse and repeat:
This flexibility of re-subscribing the bindings becomes especially useful on Android, where you can have multiple Start — Stop, Resume — Pause cycles beyond the usual Create — Destroy.
Speaking of which…
Android Binder lifecycles
You have these classes for Lifecycle right out of the box in the library:
In the above constructors, androidLifecycle is what Android LifecycleOwner returns for its getLifecycle() method, e.g. your AppCompatActivity / AppCompatDialogFragment / etc. It’s as easy to setup as this:
But let’s not stop here, because you are really not tied to Android at all. So what is a Binder lifecycle then? Whatever you say it is. Really. It can be the period while your dialog is displayed. It can be the period while some async operation is running. It can be tied to your DI scope, so that any subscription made there gets disposed along with it. You have absolute freedom here.
1. Do you want your bindings to remain alive until a certain Observable emits an element? Map it to a Lifecycle and pass that to your binder. Write this once in an extension function, reuse with ease later:
2. Do you want your bindings to remain alive until a certain Completable finishes? Not a problem, solvable similarly to the previous one:
3. Do you have some other, non-Rx environment which should determine when those subscriptions should be disposed of? Use ManualLifecycle as we’ve seen already.
In any case, you can either map a reactive stream to a stream of Lifecycle.Event elements, or you can use ManualLifecycle if you’re in a non-Rx context.
A high-level overview of your system
Binder hides the details of creating and managing your Rx subscriptions. What remains is a very concise, high-level overview of which reactive component is talking to which others in a particular scope.
Let’s suppose we have the following reactive components for the current screen:
We’d like to connect them for the scope of the current screen, and we know that:
- A UiEvent from the View can be fed directly to AnalyticsTracker
- A UiEvent from the View can be transformed into a Wish for the Feature
- A State from the Feature can be transformed into a ViewModel for the View
This can be summed up concisely with a couple of lines of bindings:
We usually extract this kind of reactive setup to its own class, and it provides an easy-to-understand, high-level overview of which component is talking to which other component in a given context.
And because we as software developers spend more time reading code than writing it, such a concise overview becomes quite invaluable — especially as the number of components increases.
We’ve seen how the Binder helps us to separate the concerns of managing Rx subscriptions, and how it gives us an overview of our system built out of reactive components.
In upcoming articles, we’ll look at what practices we apply when we’re separating reactive components of UI from business logic, and later we’ll check how to add middlewares (logging and time travel debugging) to any subscription created with the Binder. Stay tuned!
In the meantime, check out the library on GitHub!
You can also follow me on Twitter