My opinions 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 patterns like Command and Observer and makes good use of functional reactive programming techniques. In short, MVI sort of defines a meta-architecture 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 `Action`s, `Result`s; 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.
No, the biggest problem I see with the architecture is how much it hurts overall code readability and in turn, developer happiness.
In my opinion, MVI artificially groups ‘unrelated code’ close together and encourages ‘related code’ to be spread out among as many different classes as possible. That’s a big claim, let me explain what I mean.
I think we can agree that in 99.9% of the times you want to write 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. It helps to be able to understand the flow quickly without much overhead. It’s even better if the code you are looking at is isolated from any other feature. Unfortunately, this is usually how code ends up looking in MVI:
See how the flow we’re interested in is buried between other completely unrelated flows in that snippet? You probably only care about one single line from that snippet at a time.
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.
> We are never, ever, going to read that snipped of code from top to bottom (you know, like we usually read things). So why are we writing it like that?
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 makes sense. It’s OS-agnostic, it serves as an interface from the view to the business logic, enables testing, etc. In other words, it has a propose that’s beyond the architecture agenda. It allows you to A/B test different views and the VM would still work with not changes.
In the same way, `UseCase` exists because it’s a reusable component that might be used from different VMs. Same goes for `Repositories`, a repository can stand on its own without the vm and be re-used from other places. All these classes exist with a clear goal and purpose. The logic is split sensibly.
On the other hand, let’s look at MVI’s `Reducer`. A `Reducer` is specific to a single VM (not reusable) why is it extracted out of the VM then?
“For testing purposes” I hear you say. That’s not good enough of a reason IMO.
Ask yourself this: “Should I really be extracting *private* methods into a separate class 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.
Extracting logic into trivial classes that perform very simple tasks only for testing proposes results into simplistic tests that become increasingly meaningless and add a ton of friction to your codebase.
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)