MVI beyond state reducers

A modern, Kotlin based MVI architecture — I. Features

Zsolt Kocsi
Aug 8, 2018 · 11 min read

This is the first part in a series of articles on Android architecture in Badoo. You can find the next one here:

Part II — Building a system of reactive components with Kotlin

Story time: problems of state

Your application has a state at any given time that determines its behaviour and what the user sees on the UI. If you focus on just a couple of classes, this state consists of all the values stored in your variables, be they a simple boolean flag or an instance of an object. All those variables have a life of their own, changed at will by different parts of your code. It’s difficult to say exactly what state your application is in at any given point, without going through them all and checking them one by one.

When we write code, we are writing a scheme to map out our internal mental model of how a system should work. We are good at implementing the happy cases, when everything goes the way we want, but we are horribly inept at thinking about all the possible edge cases, and all the possible states our application might end up in. Getting to one of those unexpected states is only a matter of time, and then we have a bug.

When we first produced it, we adapted our code to our internal mental model. And now, going through the five stages of debugging, we painfully need to do the opposite, and adapt our own internal mental model to the reality of the actual system we produced. Hopefully, at the end of the day we leave with an “Aha!” moment, realising what went wrong — and the bug is fixed.

But not all cases are so lucky. The more complex your system is, the more there’s a chance of it entering a weird, previously unexpected state, which is a pain to debug. At Badoo, our applications are massively asynchronous, not just as a result of the functionality that users can trigger themselves through the UI, but all the one-sided server pushes that can happen as well. All of these elements have an impact on the state and flow of the application, from billing status changes to missing data blockers to new matches.

As a result of this, we’ve had quite a few weird and hard-to-reproduce bugs in our chat module, which was causing everyone a headache. QA could record a video of them happening from time to time, only to never see them again on the developer’s device. Since the code was massively asynchronous, the same order of events, the same race condition happening again had only a slight mathematical chance. And since it didn’t crash the application, we had no stack trace to even tell us where to start looking.

Clean architecture alone didn’t help us either. Even after rewriting the whole chat module with that approach, A/B tests were constantly showing a small, but not at all insignificant difference in the number of first messages sent by users using the new module compared to the old one. We supposed this was because of some difficult-to-reproduce bugs and race conditions. The difference was present after controlling the groups for every other factor, and it was hurting business goals just as much as it was hurting developers. You can’t release a new component while it’s performing worse than the previous one, but you also don’t want to work on the old one — there was a reason you wanted to move away from it in the first place. So, you need to find out why there are slightly fewer messages being sent in a system that does not crash or give any sign of behaving wrongly.

How do you even start looking for that?

To clarify, this is not the fault of Clean Architecture. This was human error. We fixed these bugs eventually, but it took us quite a long time and definitely a lot of effort. And so we wondered: is there a better way, to avoid ending up in situations like this in the first place?

The light at the end of the tunnel…

Buzzwords like MVI (Model-View-Intent) and unidirectional dataflow are nothing new nowadays.

(If they are for you, I definitely suggest googling them as there have been a lot of really nice articles about these topics. For Android, I especially recommend Hannes Dorfmann’s 7 part series: http://hannesdorfmann.com/android/mosby3-mvi-1)

We first experimented with ideas borrowed from the webdev world in early 2017. Approaches similar to Flux / Redux turned out to be really useful for us on Android, offering ways to ease the pain of the problems we were facing.

First, having all the elements of what our state consists of (i.e. all the variables we rely on to show something on the UI and trigger different flows) stored in a single, immutable State object solves a lot of the problems. For a start, now everything is in one place and is easier to understand as a whole.

For example, if we want to represent loading some data using this approach, the State needs to hold: (payload, isLoading). Just by looking at these two fields, we can see immediately if there’s anything to display (payload) and whether to show a progress loading animation on the UI or not (isLoading).

Next, if instead of parallel executions with callbacks, we model the changes of the state of the application as a series of transactions, we can now have a single entry point where it all happens. Meet the Reducer (a concept taken from functional programming), which will take the current state, some information about what to do with it, and create a new state based on the two:

Reducer = (State, Effect) -> State

Following on the image loading example above, we could have the following Effects:

  • StartedLoading

And we could write our Reducer with these rules:

  1. If the Effect is StartedLoading, create a new State from the old one by copying the old one and setting isLoading to true.

If we now logged the series of states that were created, we would see:

  1. State(payload = nothing, isLoading = false) — this is our initial state

Rendering these states on the UI would show the whole process, from having nothing to display at the start, to showing a loading animation, to finally showing the data we wanted.

This approach has many benefits.

First, by modifying our state in a centralised way with a series of transactions, we can get rid of race conditions and a lot of pesky, difficult-to-find bugs before we even start.

Next, just by looking at this series of transactions, it’s easier to understand what happened, why it happened, and how exactly it modified the state. Looking at our Reducer implementation, it’s also easier to reason about all the possible state transitions before we even launch the new feature for the first time on our device.

Another added benefit is that we can create a light UI. If we have our state stored in a single place (the Store), which takes Intents about how to change the state, applies changes using a Reducer and provides an observable stream of new states, we can now move all our business logic to the Store, and use our UI only to trigger Intents and render states.

Or can we?

…might be the train coming towards you

Having one Reducer to do all the work surely won’t be enough. What about asynchronous tasks with multiple possible results to begin with? What about reacting to server pushes? What about triggering additional jobs after a certain state change has occurred, like clearing a cache or reloading something from the database? Either we leave this logic out of the Reducer (which is like saying that half of the business logic won’t be encapsulated, and everyone else needs to take care of the wiring if they want to use our component), or we pollute the Reducer with multiple concerns.

Expectations of a good MVI framework

We definitely wanted to encapsulate all the business logic related to a business feature in one single component that requires no extra wiring, and that is as easy for other teams to use as injecting an instance of it somewhere and subscribing to its state.

Furthermore:

  • It should be able to communicate easily with other components in the system if needed.

The road from having a single Reducer to the solution we use today was not a straightforward one. The different teams were facing different problems that required different approaches, and coming up with a single solution that fits everyone did not always seem possible.

And yet, today we are happy to say that everyone seems to like what we have now. So without further ado, please welcome MVICore.

The library is open-source and is available on GitHub: https://github.com/badoo/MVICore

What MVICore offers you

  • An easy way to implement your business features in a reactive way with unidirectional dataflow

A quick introduction to Features

As the GitHub repo has proper step by step tutorials, I’m skipping detailed code examples here, but would like to introduce some of the base concepts and components.

A Feature is a central piece of the framework, meant to encapsulate the whole business logic of a component. It has three type parameters in its signature:

interface Feature<Wish, State, News>

Wish is the Intent in Model-View-Intent — that is, the way we want our Model to change (as Intent already has its own meaning in the Android ecosystem, we needed to find another name). It is the input to a Feature.

State is, well, the state of the component, as we’ve already seen. It contains every piece of information about it, and is meant to be immutable: we cannot change the values within a State, we can only create new States. It is also the output of a Feature: every time a new State is created, it will be emitted on an observable stream.

News is the event type of the component for all those signals that shouldn’t be stored in the State, but consumed only once when they happen (AKA the SingleLiveEvent problem). News are also optional (if a Feature doesn’t have any events, we can use the Kotlin Nothing type in its place).

A Feature must have a Reducer at the very least (in these cases, all inputs to the Feature, that is all Wishes, are automatically Effects that the Reducer uses to create a new State).

In addition, Features can have the following optional components:

  • The Actor encapsulates asynchronous jobs and / or conditional state modification based on the current state (e.g. form validation). The Actor maps one intent to any number of Effects, which is then passed to the Reducer.

It can be as simple in its internal workings as this:

Or as complex with all the pluggable parts mentioned above as this:

All the while the Feature itself, encapsulating all this business logic, appears as simple as this, ready to be used as a building block:

What else?

We’ve seen so far how the Feature, the centerpiece of the framework, works on a conceptual level. But the library offers so much more:

  • Since all* the internal components of the Feature are deterministic mappers, all of them can be wrapped in any kind of middleware — and the library comes with out-of-the-box implementations for logging and time-travel debugger middlewares! (* The only exception is the Actor, which is not fully deterministic since it talks to external data sources — but even in this case, at least the execution path it chooses is only determined by the input it is invoked with, with no external conditions)

In the next article, we’ll focus on the latter. You can check it out here:

Part II — Building a system of reactive components with Kotlin

In the meantime, we hope you’ll give the library a try, and that you’ll have just as much fun using it as we had making it!

You can also follow me on Twitter.

Bumble Tech

This is the Bumble tech team blog focused on technology and…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

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