The problem with MVP
The problems with current popular MV solutions and an introduction to a functional reactive solution with explicit state, commonly called Model View Intent, or MVI.
This article (among other sources linked throughout) was the inspiration for an extremely small library called kontent, it is a set of base components that aim to help reduce the boilerplate of MVI and solve the problems of current popular MV solutions.
MV architectures don’t inherently cause issues, but I believe they make it easy to write code that is difficult to reuse, test and maintain. By being aware and conscious of these common pitfalls we can hopefully write better code.
This article goes into detail describing the issues with some ModelViewWhatever solutions commonly used in Android (with examples from MVP). Then I will the briefly outline an architecture for android that helps avoid some of these common issues (by using a Functional Reactive Programming style).
By the end of this article you will be familiar with some of the issues with current popular architectures, what causes them and how a Functional Reactive architecture can solve them
Why do we need any architecuture or design pattern?
Seperating business logic from the view is hard in android, but it is very important, if you can do it well you can get some of the benifits below:
- Seperation of concerns
- Single responsibility code
- Reusable logic
- Independent of UI
- Easily testable
- Easily maintainable
- Independent of frameworks and dependencies
If you don’t think you need any architecture I would encourage you to check out Telegram’s 12,283 line ChatActivity to see how wild things can get.
The need for change — Why current solutions aren’t working.
- Ui logic often becomes mixed with business logic.
- Logic and methods often become tied to the state of the presenter so cannot be reused
- Presenter (or Controller or whatever you call it) becomes stateful and does not often have a single responsibility and neither do the functions within it
- It’s extremely difficult (if not impossible) to reason about application state (due to the multidirectional data flow and immutable data).
- Testability — the state issue makes it difficult to replicate state and therefore write meaningful tests.
- The cognitive load — caused by the large scope of functions, mutability and stateful presenters, means the code is hard to maintain and extend
An example from Google’s architecture blueprints.
If you are thinking this is a problem you haven’t encountered then I encourage you to take a look at the example presenter below. Even this extremely simple presenter from the android-architecture blueprints from Google suffers from some of these problems.
Issues with this presenter
- Side effects and mutability — mIsDataMissing is mutated by show task (see code snippet below) — now the state of the presenter is dependent on the order of methods called!
3. Separation of concerns — showTask reaches into the view to find out about state when it calls — mAddTaskView.isActive() — this means the presenter now cares about the ui state to perform some type of logic.
4. Single Responsibility — The presenter does not have a single responsibility and neither do any of the functions within it, they all depend on each other.
5. Code reuse — If you have some business logic in this presenter that your later realise you want to reuse somewhere else you will have to have extract the local variables that are affected as a side effect and that it reads from to get state and then also provide the data that is fetched from the view — mAddTaskView.isActive()
6. Cognitive load — mTaskId and mIsDataMissing are set in constructor and stored as mutable local variables — this means every time you modify or add a method to the presenter you have hold these variables in you working memory for consideration, this makes code much harder to maintain and understand and increases the chances of making a mistake.
This small example shows how just 2 mutable variables, combined with multidirectional data flow and functions with side effects, can make it very difficult to reason about state, reuse code and truly understand the code, meaning this code is hard to test, maintain and add features to.
The approach when breaking a problem down should be to have each section as focused as possible, affecting only local state, without mixing in irrelevant issues, and without side-effects if at all possible. — Christian Maioli
Every time you add a variable or method with a side effect you exponentially increase the cognitive load required to reason about the state. If these variables are enums with multiple states then this becomes very very tricky to fully understand.
Solutions — Enter MVI & functional Reactive Programming
These are 2 very popular buzzwords so lets break them down to what they mean and how they can solve some of our issues. For more info check out this awesome article.
Something reacting to an event rather than requesting data.
No side effects, a function that take an input and gives an output, it will produce the same result given the same input.
An important concept from both of these is immutable data.
If you think of both of these concepts in the context of an architecture, combined with the lessons we have learned from MVP example above, we can start to create a structure that forces us to write better code.
Reactive programming, immutable data and pure functions help us quickly create single responsible code, that is narrowly scoped only affecting local state, has no side effects and is very easy to reason about. — Dan Lew
Below is a very simple example of what a Functional Reactive stream might look like, you can see how each step is reacting to the input from the previous step and how no variables are mutated or exist outside of the stream.
It is extremely easy to imagine what state the objects will be in each step and based on the next function what the outcome will be. If this was an architecture you would quickly understand what each section is doing and the state of an input at any given moment.
One of the main issues with the MVP example above is that there is no explicit state. All of the impure functions, mutable data, and multidirectional data flow leave us with no clue about what state the view is in. This can be partly solved by having an explicit state that cannot be mutated.
Say you have a page which loads 2 scores for a team from the network and then shows them on the view:
Simply by explicitly saying what states the view can have we immediately make it easier to understand what the application is doing.
Model View Intent — Functional Reactive Programming as an architecture
Frontend-web developers used to have this problem, it was common to use Model-View-Controller (MVC) they solved this issue by using a framework called redux, an architecture based entirely around a functional approach. Below is a quote that summarizes the issues discussed in this article very well:
“The Redux architecture encourages a functional approach where you compose pure functions. Pure functions are basic functions that are deterministic in nature. Meaning that the output of a given function would always yield the same result if called with the same inputs. This is because pure functions have no internal state and leave no side effects.” —Nish Tahir
So redux is functional, but it also uses Reactive programming.Take a look at the redux diagram below, you can see how each step leads to the next and then “triggers” the next step.
This can be a great example for us Android developers, most of our current solutions encourage a functional reactive approach like this.
Lots of people have written and talked about possible architectures for android that use a functional reactive approach and solve lots of problems highlighted in the MVP example. Simply put MVI follows the to following steps (notice how similar it is to redux):
- The view generates Intens (not android intents)
- Intents are turned into actions (to provide a layer of abstraction from each view)
- These actions are processed by a function that does something, maybe performs some validation, business logic or network request and outputs a result (eg. the validation was ok or failed)
- A reducer then takes this result and produces a new state
- This view is then rendered to the user
All these steps are reacting to an intent at the start of a stream, all purple squares a immutable data type, all the yellow briefcases are pure functions.
Just like in the tiny example of a Functional Reactive stream you can see how it is extremely easy to reason about the state of any object at any given point.
For more information about implementation I can’t recommend highly enough The Contract of the Model-View-Intent Architecture by Benoît Quenaudon and his talk at Droidcon NYC 2017
In Android we have a real problem of scalability. You cannot simply make presenters, view controllers, or view model’s bigger, they don’t scale, they become impossibly difficult to understand and maintain. Methods in these presentation objects are undeterministic and difficult to reason about.
A functional reactive architecture promises to help alleviate some of these issues. There are lots of great talks and articles about MVI, so if you are interested in writing cleaner code do some googling of MVI