Once upon a time in a galaxy far, far away Android developers figured out that maybe putting everything in a single file/Activity isn’t the best idea. We have come a long way since then. We would even dare to say that of all the platforms Android developers care the most about architecture. The reason for that, in our opinion, is that we had a lot of changes and conflicting information (mostly from Google) as well as an operating system which was hacked together. So, we learned to make our apps layered/modular so we can easily switch implementations, if necessary. In the next section we will cover the base setup commonly used for Android app development.
We are big fans of “Clean Code” by Rober C. Martin. Even though it's from 2008 but it has aged like fine wine. We used it in our example as well, as you will see a bit later.
They say that a picture is worth more than a thousand words, so here’s what we did:
The general idea is that we have layers (like an onion), and every inner layer does not know anything about the outer layers. In case that an inner layer must use something from the outer layers we need to introduce the concept of Dependency inversion (not depending on an implementation but on abstraction/interface). Not to turn this into a clean code tutorial, let’s just say this is a great way to easily change single parts of the application. For the purpose of this blog, we will be changing the Presentation layer while all other layers will be the same.
Also, we prepared some examples for you so you can follow it more easily in our GitHub repository. Each of the four types of architecture has a corresponding package. We made examples to be as simple as possible not to get bogged down with too much detail.
The first popular architecture type in Android was/is the MVP one. Let’s start with a diagram:
At the center of the MVP architecture is of course Presenter. It’s a class that acts as a bridge between our View (Activity, Fragment, XML) and Model (Data, Domain, Usecase). When events happen, the View delegates it to the Presenter class which then decides what to do with it. The Presenter usually must get some data from the Model and trigger a certain View method as a side-effect of the said event.
This brings us to the biggest problem of the MVP architecture. The View knows of the Presenter and vice versa. This means they are coupled together, which brings a potential issue of memory leaks. This can happen when a long running task is interrupted, and everything is not disposed correctly. There are ways to mitigate this problem, but you need to be aware of this issue as well as write some boilerplate code to handle this properly.
Another issue is tied with the Presenter having the same lifecycle as the Activity which means it will get destroyed on orientation change. This can leak the View as mentioned in the previous paragraph as well as we must restart the presenter and any process it was doing (as well as any data that it was holding at that moment).
At the moment it was introduced, and many years later, this was an awesome tool in an Android developer’s belt. There were no obvious solutions to the issues mentioned in the previous paragraph until Google brought a new tool – ViewModel.
Let’s again start with a diagram:
The biggest difference here is the arrow pointing towards the View is now not the same as the second one. The ViewModel has no knowledge of the View and as such it cannot create memory leak problems. So how does the View get updates you may ask. Well, with the introduction of ViewModel we also get a nice new trinket called LiveData. In short, it’s an observable holder for our data which informs the View any time the dataset changes. LiveData can be used without the ViewModel but the effect is not so good due to specifics of the ViewModel lifecycle.
This brings us to the biggest selling point of this architecture and that is the special case of the ViewModel’s lifecycle. The ViewModel lives longer than the activity which means it is not recreated if the Activity orientation is changed. Basically, if we create a good responsive UI with the ViewModel we don’t have to think about what data to save and recreate on orientation switch.
It does come with some complexity though. It's not a trivial task to implement ViewModels in Dagger (you can do so more easily in Hilt and Koin but maybe you are stuck with Dagger). Also, as it is a special type of component, specifically Android Architecture Component, we need to add quite a few dependencies for it to work.
This is the most popular architecture on Android currently and that will not change until Compose sees more mainstream adoption. More on that in a separate section.
Before we start, a small disclaimer, the example for his part is rather oversimplified just to explain the core of it. There are a ton of resources online that go into much more detail (it is hard make it simple while retaining all the parts of MVI architecture like Reducers).
Let’s get to it! You guessed it, we are starting with a diagram. Check it out below:
It does look at the first glance that not much has changed compared to the MVVM architecture, but this "Intent” part is a relatively large change. First off, you may place the Intent-related code on top of either MVP or MVVM architecture, so you may build on whichever one you like best. We do recommend MVVM as it is more modern and popular these days.
So, what is MVI anyway? We would best describe it as the explicit way of defining events that happen in the app with the corresponding states/side-effects that emerge because of them. As you can see from the explanation it doesn’t delve into the segmentation or the modularization of the app or its layers and that’s why it can work with pretty much any architecture type.
To simplify, it all works this way. Imagine a user does something, and we create an Intent from it. An intent is passed to the ViewModel or the Presenter where they are processed in combination with the current state (usually it is done in a Reducer). After the reduction we are left with a new State (and potentially a side-effect) which is observed (or passed to) by View which then renders the new state. There are some in-between steps and a lot of boilerplate to make this work but this is the meat of it. One thing that is important is the strict definition of Intents and State with a special type such as Enum or a Sealed class.
It brings a lot of benefits like unidirectional data flow, immutable state (React, anybody?) and great scaling for large teams as well as well-defined screen interactions and results. Also, as State of the screen is defined in advance it is a lot easier to test.
This brings us to another issue, learning curve. If you have a team of experienced developers than no problem, but if you plan to onboard some Juniors this can be very overwhelming to learn on top of other large codebase specifics.
This time we will not start with a diagram as this section is not 100% related to architecture as such. You probably heard about Compose but just to summarize quickly:
It’s an abstraction layer built on top of the imperative style of UI. It does not concern itself with how to build the UI, but what you want to build.
Definition is from our blog which goes into much more detail on this topic. You can check it out here.
So, what does this have to do with architecture? Well, nothing and everything. Nothing, because it does not in any way change any fundamentals of app layers or stuff like that, but it does steer us in a certain direction. Compose does not mutate state but it recreates it completely which is pretty much the way that MVI architecture works. We consider MVI to be a bit to complex and has too much boilerplate but there is some middle ground there.
They it would work is to consider State as pretty much one on one with the way that Compose renders the screen. This makes it a really good match up. Also, as we must pass certain callbacks/listeners for different events that can happen we may as well define them in advance (as Intents) and with that we simplify the View part. Rest of the mapping and state updates happen in a ViewModel (probably but can be a presenter). If we do this the MVVM way, we could end up with composable that have dozens of different listeners which can be cumbersome.
We prepared a small example for this here. If you check out the MainScreen composable you can see everything you need to see. We set up a way to collect and respond to side-effects. There are also Intents being triggered by users clicks and there is state which tells the Composable what needs to be rendered. We believe that MVI simply makes sense with this new technology. Implementation could change in the future. For example, there are currently ongoing discussions on the topic of ViewModels, where one side likes the way, it works now while the other says that there is no more reason to use the ViewModel from the Android Architecture Components. But that is a topic for another article.
We covered pretty much every mainstream architecture in the Android ecosystem. There are many variations to the examples provided by us but the core of it will be the same. As you have probably noticed there is no one size fits all here. For example, we consider MVP outdated but if your legacy system is still using it and you have solved all the lifecycle issues there are 0 reasons to change: “if it ain’t broke, don’t fix it.”
If we had to pick our favorite architecture of ours, it would have been MVVM. It just works, it has tons of resources, every potential issue was addressed years ago. We know it scales well and it is maintainable in the long run.
Recruitment is also something to think about. MVVM is the mainstream (highly backed by Google as well) and as such most of the Android developers default to it.
Last thing to mention. If you are going adopt Compose any time soon, give MVI a try.