I’ve always been curious about MVI, ever since I saw the example implementation of the “Tennis Kata” using RxSealedUnions. To be honest, I’m still not entirely sure how that one works (I think it’d deserve its own article) — it looks nicer with lambdas, and it is an implementation of a finite-state state machine with strictly defined states, where the unions define the given state, and how they react in that given state.
With that union setup, invalid states are impossible.
While this is what MVI hopes to achieve with its immutable view state — to me, and maybe many others, the concept of MVI seems hard to grasp, hard to master, and hard to implement.
If you want to learn about MVI, you’ll see intimidating code and intimidating frameworks such as Cycle.js (JS), Redux (JS), Cyklic (Kotlin), and last but not least Elm Architecture written in Elm (a functional language, unlike C#, C++ or Java — which are imperative)!
And on top of that, many Redux examples love showing synchronous code where you add a TODO to a memory store with no persistence and no asynchronous operations — well, real life isn’t so easy.
But enough talk, I promised MVI, so here we go!
Deck of Cards
Luckily, Zak Taccardi (he’s a cool guy, you should follow him) put together a fairly approachable example that is based on MVI architecture (and this is pretty much a follow-up to his previous post on State Renderers).
The app itself looks fairly simple — you can tap the card to have the top of the deck dealt, you can ask the app to shuffle the deck. and you can create a new deck (which takes all dealt cards and reshuffles it).
The operations themselves are not immediate — it simulates “loading” in-between, as the “network requests” take time.
Occasionally, there is also “error” injected with a random chance, to show that sometimes, things don’t work out, and we need to handle error states as well.
Overview of components
There are a few important things to note here.
- Intentions: refers to the actions that the user can trigger via the UI.
- Presenter: subscribes to all exposed requests and operations that trigger a change in state. When a request/operation occurs, these are mapped to a type of
Change— which is used to reduce (evaluate) the new state based on the previous state and the change.
- Change: represents a type of change which determines how the State should be changed.
- State: represents the state of the application — in this case, the current Deck, if shuffling, is dealing, is building a new deck, or if there is an error.
- Ui: delegates the
render(State)call from the Presenter to the
- StateRenderer: separates the
Stateinto parts and observes changes in it individually (with a
distinctUntilChanged()filter, using a
PublishRelay), then calls the
UiActionswhich modify the UI when a change occurs in a given property.
- UiActions: represents the actions with which the UI can be manipulated. Essentially the View from MVP/MVVM.
About the Dealer…
The last remaining component isn’t strictly part of the architecture, it is an application-specific component: the Dealer.
In fact, I didn’t list it above because it behaves a bit… oddly. Instead of using
concatMap() to connect the “input” requests with the “output” modified deck or operations — a combination of
doOnNext() is used with individual subscriptions to
BehaviorRelays , but as the
deck is a
BehaviorRelay, it stores previous value!
If this were strict MVI, the only
deck would exist inside the
State, and the dealer would receive the state, and use the deck from there.
The anomaly of using
doOnNext() with relays most likely stems from that each exposed operation provides multiple events, typically both a change in deck, and a
__Operation. This could be replaced with a common sealed class, and exposing an
Observable that emits multiple events (then calls
But while the communication with the Dealer isn’t entirely stateless, we can still learn a lot from this example.
All user actions are exposed via
PublishRelays. The presenter listens to them, and maps them to changes, additionally triggering the
Dealer to do its thing.
The presenter is subscribed for events from both the
Intentions (user actions), and the
Dealer. These are all converted to subclasses of the
Changeobject, based on which a new state is evaluated from the previous state, and the new state is rendered to the UI.
Naming the actions that can change the state is the heart of all MVI solutions. In Redux, this is the
Action, in this example, it is the
Change. It represents the ways that the state can change.
The state stores the current state (unsurprisingly), but also describes how to evaluate the new state using a
Change. In Redux, this would be called the
When the object changes, a copy of it is returned, with the given variable modified based on how it is affected by the
The presenter tells the Ui to render the state, but this is actually handled by the
StateRenderer, which will call the right
UiActions to determine how to make the Ui show what we want it to show.
As we can see, the UiActions represent the
View in MVI, not much more to say about it.
The deck-of-cards example showed us a glance at MVI. Our most important take-aways are:
- Application logic is driven via exposing events, especially for interactions with the UI (
Intentions, where the name of the architecture comes from)
- Naming the actions that can change the state
- The state is explicit, and always immutable, and always copied on change
- In this case, the responsibility of “driving the Ui” is the
PublishRelayis pretty useful for cutting our stream into multiple streams! :)
For more resources, you can check out:
- KUnidirectional (an example without Rx!)
- Hannes Dorfmann’s writings on MVI
- this video by the guy behind Cycle.JS (which is the inspiration behind the MVI variants on Android)
- this video by the guy behind Redux (which is very similar to MVI) on how he turned Stores into Reducers
- this talk by Jake Wharton on managing state reactively (and properly!)
- (maybe also this talk by Christina Lee on ViewDrivers that maybe one day we’ll also understand, along with this proposition behind it)
Special thanks again to Zak Taccardi for writing an example that can be understood once looking through it properly.