Just add MVI with Orbit 2

Mikołaj Leszczyński
4 min readSep 12, 2020

--

We love the MVI architecture concept.

It brings clarity and consistency to our codebases. We wish to share our best knowledge of simple, no-nonsense MVI for Kotlin, Android and beyond. It fits both small and large projects well. We hope you find the library we created useful to you and makes your life a bit easier.

We spent the last few months building a much-improved version of our MVI library, Orbit 2, taking the best features of Orbit 1 and rewriting the rest from scratch.

Let’s jump straight into the action to show you how easy it is to build MVI into your application. We will load a list of posts to form a Twitter-like feed.

Creating an Orbit ViewModel

First we include the Orbit library in Gradle:

To get an Orbit-enabled ViewModel, implement the ContainerHost interface. Then, use one of our handy factory functions to create the Orbit container.

Now let’s load some data.

Loading data on container creation

A typical use case on Android would be to run an initial request to load data when you create the container for the first time. Orbit supports this use case through a lambda you pass to the container factory function:

After setting that up, we can invoke Orbit to launch a network request.

Let’s unpack what happened here. We can distinguish Orbit operators used:

  • The orbit block allows you to build and run MVI flows when you call a function.
  • The transform operator allows you to map upstream events. In this case, there is no upstream, so all we do is make a blocking network call. All transform operators execute on an IO thread pool.
  • The reduce operator reduces upstream events with the current state of the container. The result is a new container state.

We can optimise this further but for now, let’s connect our UI to the ViewModel.

Connecting the ViewModel to Android UI

The easiest way to connect our ViewModel is via Flow .

Invoking actions on our MVI system from the UI is easy - just call a function on your ViewModel. As an example, we will make the pull to refresh gesture load the overviews again:

Your ViewModel functions are plain old functions, so you can pass parameters through if needed as normal. Your MVI flows can then use these parameters.

Optimising the ViewModel

Blocking calls, even offloaded to a background thread, are not the best practice. Fortunately, the Orbit library is extensible and modular. We provide extra operators via modules.

Orbit 2 design

We have big ambitions for Orbit. Yet, the design decisions we made when we first wrote what we now call Orbit 1 held us back from achieving our goals. Above all, we value simplicity and developer experience. While devs loved working with Orbit 1, we felt we could simplify it further. Testing was also another area of improvement due to it being quite cumbersome.

Photo by Marc Reichelt on Unsplash

Times have moved on. Coroutines are now officially recommended by Google for asynchronicity in Android. We just had to support them in Orbit. At the same time, we did not want to lose RxJava compatibility. The old architecture meant having both was not possible.

One fundamental shift in Orbit 2 was doing away with using streams for everything. We can represent the MVI concept via one big reactive cycle. In that cycle, network requests are mid-cycle transformations. The suggestion for RxJava MVI implementations was to use ObservableTransformers. That is, in our opinion, overcomplicated. The reality is that 95% of use cases only ever emit one result. For this, you do not need streams. Why make 100% of your code complex to support 5% of corner cases?

The most common use case is that one UI action sets in motion a one-off MVI flow (e.g. a network request). Orbit 2 makes that use case the primary focus to inform the developer experience. Worry not, we still allow you to connect streams where you need them.

The result is a clear and concise syntax with 1–1 mapping between UI actions and MVI flows.

MVI architecture can describe complex systems in simple terms. Indeed, we don’t think we should limit its usefulness to UI. The platform you use it on should not be a limitation either.

We currently support only Kotlin and Android projects. The JetBrains team has put hard work into making Coroutines multiplatform. That enables us to consider taking Orbit to other platforms.

All the pain points above guided us to Kotlin Coroutines as the basis for Orbit 2.

Wrapping up

We hope we piqued your interest and you found this post informative. That was just a taste of Orbit’s capabilities!

Orbit 2 in a nutshell:

  • Integrates best practices from our 2+ years of experience with MVI
  • Powered by Coroutines
  • Easy to use, type-safe, extensible API

Works with any async/stream framework

Orbit ❤️ Android

Testing

Documentation and more complete samples are available below

--

--

Mikołaj Leszczyński

Software Architect at Babylon Health. Previously at Just Eat. Enjoys fixing complex technical problems with simple solutions.