Reactive (& functional) programming using RxJava is gaining momentum on Android these days. Even though it has been almost half a decade ever since we started using RxJava, we are still figuring out better and nicer ways to organize reactive code. On the other hand, the JavaScript community has been innovating and evolving rapidly in this area.

I got hooked into MVI after watching André Staltz’s presentation on the topic. He has also authored Cycle.js, a UI framework for building reactive applications using JavaScript. Most of the ideas that I am going to discuss here are from Cycle.js and Redux. The implementation of these concepts are influenced by the works of André Staltz, Dan Abramov, Paco, and Jake Wharton. I also believe Hannes Dorfmann was the first person to write about MVI on Android. If it wasn’t for him, I can’t imagine how I would have discovered the contributions of all these brilliant people.

Having said that, there are enough UI architectural patterns out there. Do we really need another? How is MVI any different?

1. Predictable

Cycle.js - A functional and reactive JavaScript framework for predictable code. Redux is a predictable state container for JavaScript apps.

Both Cycle.js and Redux focus on building apps that are predictable. When I say predictable, it means apps that behave consistently even under unfavorable conditions and are easy to test. Both these libraries achieve predictability by adopting some functional programming techniques and also by streamlining how the application state is modeled and updated.

In an imperative world, we solve problems simply by mutating the application state. This works, until we have to deal with asynchronicity. User interfaces are event-driven and are asynchronous in nature. By embracing immutability, we can play nicely with the inherent asynchrony exhibited by user inputs, platform behaviors, sensors, network calls, disk IO, etc.,

Predictability is also achieved by simulating faults during development that can be difficult or impossible to replicate in real-world scenarios. Hardware failures, a combination of IO failures, etc., are easy to simulate and handle in RxJava. In fact, the architecture “forces” us to treat exceptions as normal. This is contrary to the notion of building the golden path first and then preparing to handle failures.

2. Platform Agnostic

Most of the times, Android and iOS applications tend to look and behave similarly (apart from their platform’s UI/UX and capabilities). If you have worked on a product that has apps on both the platforms, it is not uncommon to see business logic being duplicated or apps exhibiting different behaviors due to a disjoint in communication between teams. Which in turn, results in defects being raised in your issue tracker. MVI on the other hand, enables collaboration between teams* because the architecture focuses more on the bigger picture and less on low-level implementation details. In addition to that, Rx itself is a cross-platform specification.

These qualities have some interesting side-effects,

Code reviews can happen between Android (Kotlin) and iOS (Swift). Code sharing is easy because Kotlin and Swift look so similar and porting code from one language to another is a walk in the park if you are using reactive logic throughout the app. I would like to see where this leads to when Kotlin/Native hits 1.0.

3. User-centric

Discussions before building features on other architectural patterns usually start with use-cases and then gradually lean towards the nitty-gritty details of layers, domain models, classes, methods, etc., There aren’t much discussions about the user and sometimes even non-existent. We focus mostly on how to build the moving parts and after that on how to coordinate them.

MVI puts the user right at the heart of discussions simply because, intentions (I) are inputs from the user and the view (V) is an output for the user. Two-thirds of the architecture’s abstractions are about the user already.

One of the things that I noticed while using MVI was a shift in developers’ vocabulary. During discussions, majority of the conversations would revolve around the user — “what does the user want to do here?”, “what would the user see when he does this?”, “Is this what the user really wants to accomplish?”, “how would the user feel if this fails?”, and so on. Almost, every use-case in MVI starts with the user’s intention to get something done. I haven’t come across any other architectural pattern that puts the user in the front-line. There was also more empathy for users in general while working with MVI.

4. Reactive

Two popular architectures that are used with RxJava on Android are MVP and MVVM. These architectures are well-known and battle-tested. If we look into the current situation closely, you’ll notice that we are taking one of these imperative architectures and then adopting it for reactive programming. When we do that, chances are we may not be utilizing reactive programming’s capabilities to its fullest. There is nothing wrong with using MVP or MVVM along with RxJava. If it works for you, then that’s great. Developers have built complex software using these architectures. However, if we can do better, then why not?

MVI was conceived and tailored specifically for reactive programming. Unlike other architectural patterns, all layers and the interactions between them is facilitated through reactive streams. Thereby making the entire application reactive from the ground up. With a reactive architecture, it is easy and convenient to build apps that conform to the principles discussed in the reactive manifesto.

5. Unidirectional

One way to think of MVI is to think of it as a chocolate factory where cocoa seeds along with other ingredients are transformed into delicious mouth-watering chocolates. Raw ingredients are sent through several stages where they are filtered, transformed and combined in each stage until they end up as chocolates. There are a couple of interesting observations in this analogy,

Ingredients move in a single direction from one stage to another, there is no back and forth. The output of one stage is input to the next. For example, the roaster takes raw cocoa seeds as input and produces roasted cocoa seeds as output. These roasted cocoa seeds are inputs for the crusher, which in turn produces cocoa powder as output. So on…

In MVI, the data always flows from one part of the program to another in a single direction. This makes testing and debugging the app easy because the inputs and outputs of a given component or function are always well-defined and predictable.

Core Idea

The programs that we create consist of code that can be broadly classified into two categories — logic and effects. Checking if an email address is valid, calculating the rate of interest, finding people who own dogs in a neighborhood are all logic. Effects are things that change the external world. Things like drawing gummy bears on the screen, writing to the database, making network requests, etc., are effects.

Usually, the programs that we write have logic and effects blended together. If we were to visually represent it, it may look like this —

Logic and effects blended

When logic is blended with effects, it becomes hard to test either. What we want is a way for us to separate logic from effects so that they can be built and tested independently. Reactive streams enable us to accomplish a nice clean separation between logic and effects and thereby help build software that look more like this —

Logic and effects separated by reactive streams

If you are interested to learn more about this idea, watch André Staltz’s video — The Return of Stream I/O.

Model-View-Intention**