Gabor Varadi

@Zhuinden

A glance at MVI through a deck of cards

from https://lambda-it.ch/blog/post/reactive-data-flow-in-angular-2

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.

Tennis Kata: scorePoints() by Pakoito

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).

Picture of the Deck of Cards app

The source code is available at Zak Taccardi’s Github repository (ZakTaccardi/deck-of-cards), and it is written in Kotlin.

However, if you feel uneasy with following Kotlin, then luckily, I took the time and converted the code to Java, which can be found under my name in a fork ( zhuinden/deck-of-cards ).

.

.

.

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

Components and their interactions with one another

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.
  • StateRenderer: separates the State into parts and observes changes in it individually (with a distinctUntilChanged() filter, using a PublishRelay), then calls the UiActions which 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 flatMap() or 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 onComplete()).

But while the communication with the Dealer isn’t entirely stateless, we can still learn a lot from this example.

Intentions

Intentions (user actions)

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.

Presenter

Presenter

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.

Change

Change

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.

State

State

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 StateReducer.

When the object changes, a copy of it is returned, with the given variable modified based on how it is affected by the Change.

StateRenderer

State Renderer

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.

UiActions

UI Actions

As we can see, the UiActions represent the View in MVI, not much more to say about it.

Conclusion

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 StateRenderer
  • Apparently, PublishRelay is pretty useful for cutting our stream into multiple streams! :)

For more resources, you can check out:

Special thanks again to Zak Taccardi for writing an example that can be understood once looking through it properly.

Source code is available in Kotlin by Zak Taccardi or in Java transcripted by me.

More by Gabor Varadi

Topics of interest

More Related Stories