Building MVI apps with Roxie

James Shvarts
ProAndroidDev
Published in
8 min readJan 17, 2019

--

https://unsplash.com/photos/JI0KxozvOtQ

Unidirectional Data Flow (MVI) architectural pattern is a relatively new but rapidly growing approach to building Android apps. It has been getting a lot of attention from the community lately with companies like Square, Airbnb, Groupon, Spotify, and many others embracing it. And for a good reason —it works. This post will not cover why Redux-like architecture works (there are plenty of posts on the subject out there). Instead, I’d like to introduce our implementation of this architectural pattern and how we arrived to it.

Our Journey to MVI

One of the best aspects of Android development ecosystem is a strong open source community around it. Quite a few companies and independent Android developers out there saw the value in Unidirectional Data Flow/MVI approach and built libraries, wrote blog posts with code samples around this architecture.

Our team at WW explored this reactive pattern to see how it can help take our codebase to the next level. Here are some of the goals that we set out to accomplish with this library:

  1. We did not want to depend on a single existing library that permeates our entire codebase and drives our architecture. Instead, we wanted to build a framework/library to address our specific needs and team composition.
  2. We wanted to limit the amount of RxJava necessary to keep the code readable and maintainable first and foremost. This includes not using custom operators, multicasting, ObservableTransformers, as well as keeping RxJava out of the UI layer whenever possible. This was important to us to avoid making the code unnecessarily complex causing a steep learning curve, introducing bugs, etc.
  3. We wanted to allow for flexibility where both Activities and Fragments could be observing view states yet avoid too much of inheritance and, in general, code bloat. You’d be surprised how many major players’ libs don’t have this flexibility.
  4. We were careful not to make the library too rigid, where too much is hidden away, to avoid taking freedom (and fun) away from our development.
  5. We needed the architecture to be easily testable.
  6. We wanted to support Process Death handling by being able to initialize the state machine to the last known state (instead of always starting with an idle state, for instance).
  7. We wanted built-in support for meaningful logs so that data flow can be better visualized and tuned during development, testing and even in production.

Introducing Roxie

Roxie, https://github.com/ww-tech/roxie, is our take on the MVI architecture. We have been using it at WW with great success. When building Roxie, we drew a lot of inspiration from other open source libraries so we’d like to give back by open sourcing it now.

With only 50 lines of code (excluding comments) and a method count of 60, it’s safe to say that Roxie is the lightweight alternative to other unidirectional implementations out there.

Even if you don’t plan on using the library in your apps, the documentation and the small footprint of the library should help you grasp the core MVI concepts as well as related RxJava operators.

There is a decent amount of detail on using Roxie in the sample app as well as the Wiki pages to get you started. Let’s cover some basics here.

Roxie is Kotlin first and Kotlin only. Coding MVI screens in Java would be doable but not very pleasant— too much boilerplate. Kotlin’s data and sealed classes really shine here.

Roxie in action

First things first: let’s define MVI:

  • Model (aka State)
  • View (aka UI)
  • Intent (aka User Intention — not Android Intent!)

In Roxie, to avoid the “Intent” confusion, we call it Action.

Data flow with Roxie

You get a single pipeline to dispatch new Actions and a single pipeline to observe new States. This consistency makes it easy to code and unit test each feature.

The flow can be summarized as follows:

Let’s see what it looks like in the code based on a small easily-digestible example:

Reducer

Here is a Reducer definition. It combines the current State and Change to produce a new State. It is a pure function which creates no side effects. For a given input, it always produces the same output which makes testing easy. A Reducer becomes a single source of truth for States managing a finite state machine.

Action

Actions are user interactions with the UI. Each Action can produce more than one Change. For instance, an Action may result in a Loading Change followed by a Data Loaded Change. So there is a one-to-many relationship between Action and Changes.

I find that MVI nudges you toward designing your Actions, Changes, and States so your edge cases are accounted for early in the process and not as an afterthought (or, worse, when a bug is reported).

Actions are immutable.

Change

A Change is usually (but not always) a result of a Domain layer interaction. For instance, loading a NoteDetail would result in a call to a UseCase or an Interactor to look up note detail based on note ID.

Changes are immutable.

State

A State generally represents a screen. Reducer produces a new State based on a previous State and a Change. So there is a one-to-one relationship between Changes and States.

One of the core concepts enforced by this architecture is an immutable State. New State is generated from an existing one by applying a Change (Result of user action). Once State is created, it’s never mutated — this is what makes this architecture predictable.

You could code your States as sealed classes instead if you like.

Do you need to share State data between several Fragments? No problem — just make their host Activity the LifecycleOwner.

Logging

You can configure logging with Roxie to log your Actions and States automatically. If some of them contain sensitive info such as user info, you can obfuscate this data by overriding your data class toString() function. There is a section on Logging in the Wiki.

Logging lets you visualize your data flow and help during development while writing tests and even in production when you investigate an obscure crash or a bug. With this data, you should be able to reproduce your current State and a given Action that caused the crash.

Sample log with Roxie

ViewModel

Roxie uses ViewModels and LiveData from Architecture Components. We chose this architecture to manage State to maintain the latter between screen rotations and other configuration changes. Alternatively, we could have some sort of a Presenter and manage the latest State with replay(1).autoConnect() but why make your life harder?

On line 52 we use switchMap() operator so that if we are currently processing one Action.LoadNoteDetail and a new Action.LoadNoteDetail arrives, we discard the previous one before processing the new one.

Admittedly, RxJava is used heavily here but most of the operators simply comprise business logic steps. We think it’s quite readable and is a good gateway into functional and self-documenting code. Also, you may notice that RxKotlin (Kotlin extensions library for RxJava) is used as well — we believe that it makes the code even more readable.

Note the use of state::postValue when setting a new State. In some cases, state::setValue will need to be used even though both are done on the main thread. Learn more at https://github.com/ww-tech/roxie/wiki/4.-States#setvalue-vs-postvalue

Observing States

A Fragment or an Activity LifecycleOwner can observe our ViewModel for new States via LiveData<State> (line 56 below). When a user interacts with the UI new Actions are dispatched via viewModel.dispatch(MyAction) (lines 61 and 65 below).

Traditionally, in many MVI implementations on Android, Actions are called Intents and they are emitted in an Observable stream. We prefer to keep RxJava out the UI layer. Occasionally, for more complex screens we do use RxBinding (which helps perform validation, debouncing) and afterward, dispatch Actions into ViewModel using the API above. If you have comments about this approach, please comment below.

With this design, if you are tasked with resolving a bug, the first thing you’d probably do is to ensure that you have correct Actions and States flowing thru the system (you can utilize logs for that). If the Actions and States are what you expect, the next place to look would be the rendering logic inside the fun renderState(state: State) (line 69 above). The architecture makes it much easier to troubleshoot various issues.

Maintenance and Scalability

With MVI each feature tends to be in its own namespace (package, module, etc.) with its own set of Actions, Changes, States, and Reducer. This may seem like a lot of extra code to write but given how expressive and concise Kotlin is (think data and sealed classes), it’s not a big issue at all. The benefits greatly outweigh the disadvantages: keeping features isolated makes it easy to write and maintain them. And you tend to better design UI interaction use cases and their possible outcomes thus accounting for edge cases early in the process.

Testing

The beauty of this approach is consistency and ease of testing. Essentially, you’d need to test the following:

Given initial State and a mock Change,

When a particular Action is dispatched,

Then assert that certain State(s) were emitted in the correct order

Tests are fast as they run on the JVM. mockito-kotlin makes tests easy to read and write.

Once you have a test blueprint for your ViewModel, it’s easy to apply the same test formula to other ViewModels. This means writing better tests and writing them faster!

Conclusion

Since we started using Roxie, the overall code quality went up, test coverage improved and pull requests became more thorough. Consequently, we are able to concentrate on features more and having to solve fewer bugs.

Give https://github.com/ww-tech/roxie a try and let us know what you think! If you can think of ways to improve the library, let us know here or by opening a ticket or creating a PR! The sample app and the Wiki pages should help you to get started as well.

WW is looking to hire Android Engineers in NYC and San Francisco. Do you want to work with cutting edge libraries on an app with minSdk of 21? If so, do apply!

Many thanks to Michael Carrano, Joseph Tran, Hyunwoo Park

Thanks to Android Weekly newsletter #349 for featuring Roxie!

Visit my Android blog to read about Jetpack Compose and other Android topics

--

--