A case against the MVI architecture pattern

Fernando Raviola
7 min readApr 13, 2021

FOR AN UPDATED VERSION OF THIS STORY SEE: https://dev.to/feresr/a-case-against-the-mvi-architecture-pattern-1add

My opinions here are based on my own experience working with different implementations of MVI applied to both personal projects and at my day job.

To begin with, let me say that MVI looks very appealing at first sight. It combines battle tested patterns like Command and Observer and makes good use of functional reactive programming techniques. In short, MVI sort of defines a meta-architecture favouring classes instead of functions and where every action the user can take is defined by an Input class and every state a view can be in must also be defined by a subclass of the State class.

I’m assuming you already know how this works, there are more classes like these in MVI including Actions, Results; among others, depending on each specific implementation.

The problem with MVI

My biggest pet peeve with this architecture is not how boiler-platy it can get, or how opinionated certain implementations can be.

My biggest problem I see with the architecture is how much it hurts overall code readability.

In my opinion, traditional MVI implementations artificially group ‘unrelated code’ and encourages ‘related code’ to be spread out among as many different classes as possible (low coupling, high cohesion)
That’s a big claim, let me explain what I mean.

When writing a new piece of code, you only care about the feature you are writing/reading and not much else. If you are fixing a bug, you are usually focused on a specific flow where the bug is happening, trying as hard as you can to reproduce it. The faster you can build a mental model of how this feature works the faster you’ll fix the bug. So it helps if the code for the feature grouped together and isolated from any other feature.

See how “your flow” is buried between other completely unrelated flows in that snippet? You probably only care about one single line in that method at a time.

We are never, ever, going to read that snippet of code from top to bottom (you know, like we usually read things). So why are we writing it like that? It’s hard to write, it’s hard to read.

You might be thinking “well, that doesn’t looks so bad”. Now imagine 10 or 20 more lines in the when statement, which basically only perform a trivial mapping from two very similar sounding classes FooInput -> FooAction.

It doesn’t stop there though, in many implementations actions get mapped to results, and then results get mapped to states:

Take a moment to think how you would follow the code in your head while trying to find that nasty bug. You start from the view, jump to the view model, and see the first mapper (`inputs` -> `actions`). From there there’s not clear path to take (or IDE shortcut to use) since the architecture usually hides the wiring from the clients to jump to where the flow continues.

You basically need to do a `cmd+f` of the `action` name (or `cmd+b` to get a list of places where it’s used/declared) to try and find where that action is being processed, that’s usually in the VM as well, some implementations define a `UseCase` and place the mapper there, it really could be anywhere.
Ok, you found the `Actions` -> `Results` mapper, now you jump again this time to the `Reducer` and you find… another mapper. At this point you forgot what you where doing.

The architecture is forcing you to traverse the code in the opposite direction that is being written. It’s analogous to reading a book where each next word is in the following page.

And that’s my issue with it! This added overhead makes it easy for bugs to hide in plain sight. This gets more tricky when you take into consideration RxJava/Coroutines, Asynchronous events, Lifecycle scopes, etc.

> MVI forces us to jump from place to place in order to understand a single flow.

I understand where MVI is coming from here though, if multiple `Inputs` produce the same `Action` then we can simplify things a bit. We can break the dependency between `Inputs`, `Actions` and results. Unfortunately, in reality this is almost never the case. Most of the time, these flows don’t share a common `Input`, `Action`, or `Result`, and for every “flow” in your app, you’ll have one of each.

Don’t get me wrong. There are valid reasons for splitting code.
A good example of this are ViewModels. A ViewModel exists not because a particular architecture requires it, it exists because it has a clear purpose. It serves as an interface from the view to the business logic, separates the rendering view code from non-rendering related code, it signals a larger scope (at least in android the lifecycle-scope of the VM is slightly larger than the scope of the views), facilitates testing, etc. In other words, it has a propose that's beyond the chosen architecture agenda. It has the potential to be re-used, you could A/B test different views using the very same VM and everything would still work.
In the same way, UseCase exists because it's a reusable component that might be used from different places. Same goes for Repositories, a repository can stand on its own and serve multiple clients. All these classes exist with a clear goal, scope and purpose. Here, the logic is split sensibly.

On the other hand, let’s look at MVI’s Reducer. A Reducer is very specific to a particular VM (not reusable). Moreover, the scope of the reducer is the same as the scope of the VM. Why extracting it out out into its own class?
"For testing purposes" I hear you say. That's not a good enough of a reason IMO.
Ask yourself this: "Should I really be extracting private methods into a separate classes solely to be able to test them?" I personally don't think so, those methods are private for a reason, they are implementation details, they are not meant to be unit tested directly. Otherwise, what's stopping you from extracting every single private method from every single class in your project and testing those too?.

Reducer is a leaky abstraction

It’s perfectly normal to want to simplify your classes when the get too big and complex. But in my opinion, extracting things into different classes needs to be done in a sensible way.
Splitting your VMs based on functionality/use-case/features:
// MediaCache: This class handles caching for images and video
makes more sense than splitting it by some arbitrary architecture convention ie
// Reducer: This class reduces results to states (?).

Testing MediaCache makes a lot of sense, this class might be used from multiple clients, it manages state and can have its own lifecycle (it can be cleared if the user logs out for example). The same cannot be said for the Reducer whose scope is equals to the VM scope.

Another reason the reducer is a bad abstraction is that it could potentially grow infinitely since it could host logic for any new feature you add to the View/VM. There aren’t many operations that you can put into MediaCache, it has a well defined scope. Sure, you can get very fancy and design a very complex cache mechanism but all your code will be within scope. Not the same with Reducers, the scope can grow as you add more and more new features.

Extracting implementation logic into trivial classes only for testing proposes results into simplistic tests that become increasingly meaningless and add a ton of friction to your codebase. If you want to read more about why testing private methods is bad I highly recommend this read: Unit testing private methods by Vladimir Khorikov

When you have a class with a single function reduce and no state, you should ask yourself why is this a class to begin with. Just move the function inside your VM, no need to extract it into artificial classes.

Ending thoughts

There are many other reasons I dislike this architecture, It’s very intrusive, it adds a ton of boilerplate, it can be ambiguos, etc. Those are not exclusive to MVI and I could live with some of those drawbacks. I’m sure MVI might fit some applications better than other, but in all cases, readability suffers.

All in all, MVI looks elegant from a theoretical standpoint but I have yet to see an implementation that actually works
and scales well for big projects.

I’ve very happy that Jetpack compose is just around the corner! Up until now I’ve been using nested custom views + coroutines and structured concurrency in my apps (something that kind of resembles the composable approach that Google is taking with Jetpack Compose)

[Website](https://feresr.github.io)

[Twitter](https://twitter.com/fernandoraviola)

--

--