A Guide to Choosing the Best Architecture Pattern for Android Apps

ProfilePicture of Abdurahman Adilovic
Abdurahman Adilovic
Senior Mobile Developer
Two developers adding components to an Android app in a giant mobile phone

Just like all software, Android applications should adhere to widely-accepted architectural principles and patterns. Without a solid architecture, Android apps can quickly become difficult to maintain, as their Activities and Fragments may lack a coherent design or a consistent set of behaviors. This is why it’s crucial to have skilled Android developers who can effectively implement these architectural patterns.

Considering the importance of good architecture, how should you select the right approach for your project? In most situations, I recommend beginning with Google’s standard architecture for Android apps as a foundation. As the app’s complexity increases, additional concepts can be incorporated. So, what does Google suggest as a starting point for an Android app? It’s quite straightforward, with a few key guidelines to follow:

However, there’s more to consider. For instance, I believe incorporating Dependency Injection is essential, and there are several options available. In this article, we’ll explore the strengths and weaknesses of prevalent Android app architectures currently in use, aiming to help you determine the most suitable approach for your next Android application project.

Table Of Contents

What Makes Software Architecture Good?

Before focusing on Android, I’d like to talk about the effectiveness of different software architectures in general. Based on my experience, three things are necessary for a software architecture to be successful, regardless of the platform:

3 key aspects of successful software architecture.

Let’s explore each point in more depth.

Simplicity

If an architecture has too many moving parts, it becomes difficult to work with both at a conceptual level and a technical one. Let’s say that RxJava is used as a concurrency framework in a project, that single dependency would mean that any new developers coming to the project are required to know RxJava, which is not a trivial matter to learn. Additionally, it becomes another piece in the puzzle that may lead to future design and debugging problems. As complexity grows, the problem compounds. The less abstraction the architecture has, the easier it becomes for engineers to get up to speed and maintain a project, which brings us to our next point…

Maintainability

With this principle, the main idea is to depend as little as possible on the “outside world”. For example, let’s say an app is developed using MVP (Model View Presenter) with a Repository pattern architecture. If the repositories hold a direct reference to something that is Android-specific like AsyncLoaders, then each repository class is coupled with the AsyncLoader class. The AsyncLoader class is a Google-maintained library which was deprecated at some point, making all of its dependents also deprecated in a way. This risk becomes greater when using dependencies without a strong community behind them because it is more likely that they’ll become deprecated. This kind of coupling is cumbersome and can make a project difficult to maintain as time passes and compatibility issues begin to arise.

Flexibility

This principle is mainly about ease of change. For example, can the UI layer of an app evolve independently of its business logic or data layer? If everything is crammed into Activities or Fragments that would make the project brittle, and implementing new features would require editing huge classes which could lead to the introduction of bugs. Splitting things into layers would prove beneficial down the road when new features are implemented, like taking the business logic out of the UI layer (Activities or Fragments) and moving them into something like a Presenter. When logic is appropriately separated, changing one part of an app without impacting the others becomes easy – a good architecture should support that.

MVC (Model – View – Controller) Architecture

One of the oldest and most widely-used design patterns in software architecture is MVC. It has a strong separation between the View – how to present data, the Model – how to structure the data, and the Controller – how to handle user interaction. For the most part, Android is designed so it can follow the MVC pattern. However, the problem with Android’s MVC implementation is that the Activity is both the View and the Controller, which violates the single responsibility principle that is key to this architecture.

Diagram of MVC Android app architecture

MVP (Model – View – Presenter) Architecture

Something that the Android community started to utilize more and more was the MVP pattern, where business logic was defined inside classes called Presenters. The Activity would inflate the View and interact with the Presenter which would inform the Presenter of user actions. This setup proved to be very effective since the business logic is nicely isolated and the View can be swapped out independently of Presenters. 

Diagram of MVP Android app architecture

MVVM (Model – View – ViewModel) Architecture

There was a problem with the MVP pattern, it was not reactive in any way. A lot of boilerplate had to be written to connect model updates to the View. Looking for potential answers, the Android community realized that a reactive approach would be simpler and more effective for mobile architectures. ViewModel is a model that the View observes and each time a model changes the View will update itself. This is where Google’s data binding library comes in – it wires LiveData from ViewModels to the Views automatically. 

Diagram of MVVM Android app architecture

MVI (Model – View – Intent) Architecture

For a more granular approach, a concept of an Intent can be introduced. Each user interaction is an instance of a set of Intents that define an app’s screen. Each change on a screen is encapsulated in another Intent which is fired back from a central place such as a Presenter, Controller, or state machine. The primary idea here is to provide a unidirectional data flow (UDF), where data and screen changes are coming from one place while flowing in a single direction. 

Diagram of MVI Android app architecture

Out of all these options, what does Google actually recommend? Model View ViewModel (MVVM). Let’s dive into the basic building blocks of an MVVM toolset.

Jetpack

Jetpack is Google’s toolset for building and architecting Android apps. It consists of many different libraries freeing developers from the burden of writing those tools themselves. Some of the most used components are LiveData, ViewModel, Data Binding, Navigation Component and Room.

MVVM as a Reactive Architecture for the UI layer

What does a typical screen consist of in the MVVM’s setup? On the low level, there is a View, Activity/Fragment and on top of that, there are ViewModels that expose LiveData. Going beyond ViewModels we can find Repositories that typically utilize Room to store data locally. 

Diagram of MVVM as a reactive architecture for the UI layer

ViewModel as a First Cache Layer

ViewModels are special classes that provide an elegant solution to the common problems found when the screen is rotated. Every time a screen is rotated, the top Activity/Fragments are destroyed and recreated again, and data that is visible on the View is lost. ViewModels solve this problem by surviving orientation changes which make their scope bigger than the scope of an Activity or a Fragment. This also has a nice benefit of being a first cache layer, when a ViewModel is active and a new View is created, that View can just grab the data from the ViewModel without any network calls. 

LiveData

ViewModels typically expose reactive sources of information known as LiveData. LiveData is a special class that knows how to talk to LifeCycleOwners like Activities and Fragments. Since ViewModels outlive Activities and Fragments, special care has to be made when Views are updated and a ViewModel should not directly hold references to any Views. This is where LiveData can be very useful; LiveData can hold observers internally and know each time an observer (View) is destroyed. When a View is destroyed it’s removed from the observers array which will prevent any null pointer exceptions that can be common in the Android world.

Repository as a Data Source and a Second Cache Layer

Repositories have one responsibility and that is to provide data for the ViewModels. Repositories can be simple, they can just fetch and pass on data from a network, or they can cache and store data locally. A common pattern is to fetch data from the network and cache it locally to reduce the number of network calls and to provide an offline experience to users. 

Diagram of a repository as a data source and a second cache layer

Database Source

A repository can store its data locally. There are a multitude of choices when it comes to local persistence; Jetpack recommends Room library which takes care of creating SQLight tables and provides easy methods for storing and retrieving data

Network Source

When it comes to fetching data from the network, Google’s recommendation is the Retrofit library. It works with coroutines and multiple response types like JSON, XML, etc.

Network Bound Resource

A network bound resource is an implementation of the repository logic made by Google’s engineers where the logic of caching data locally using Room is implemented. The basic resource returned by NetworkBoundResource class is a Resource generic class that can hold data, statuses or errors.

The Benefits of this Android Architecture

Pros and cons of Google's recommended Android architecture.

Fixes Lifecycle and Configuration Change Bugs

With ViewModels that can survive configuration change, the majority of the rotation bugs are fixed. LiveData solves the issue of updating a destroyed View, which in turn simplifies the logic of fetching new data. Developers don’t have to worry about Views lifecycles, they can just grab the new data and store it in LiveData.

Decouples Activities and Fragments from Business and UI Logic

As a bonus, ViewModels are now a central place for fetching data in apps, and all of that code is moved from Activities and Fragments into ViewModels. This approach can scale very well since it’s very simple to reuse ViewModels across different Activities or Fragments. It’s also very simple to use multiple ViewModels in an Activity or a Fragment; this is very important since the app’s logic can be split into multiple, single-purpose ViewModels.

Disadvantages of this Android Architecture

Coupled With the Android Framework

All the classes we mentioned are tightly coupled with the Android Framework. That is especially true when it comes to testing. A special testing framework has to be used in order to test LiveData and ViewModels which is not ideal. If the ViewModels are coupled with the app’s business logic it can be problematic if Google decides to deprecate ViewModels. 

Repositories Don’t Fit In Very Well With Mobile Apps

Repositories should be responsible for fetching data, but with Android apps, lots of different things happen besides fetching data, for example checking permissions, using apps local storage, background processing. Something like UseCases can be incredibly useful here, they can represent all of the app’s use cases in a decoupled manner. UseCases can utilize repositories to hold data but essentially they hold business logic of the app, decoupled from the Android framework. 

What’s Missing From Google’s Architecture Recommendations?

Dependency Injection

While Google does mention Dagger in the Jetpack documentation, I would not recommend it for simple apps because it’s very bloated, complex, and difficult to grasp. There are some strides made by Google to simplify Dagger by introducing Hilt. Dagger is a heavyweight of the DI tools on Android and should be used sparingly. I recommend considering the following alternatives:

Koin

Koin is a simple and fast dependency injection library for Kotlin, with special helpers for Android apps. It’s so simple yet so powerful that I recommend Koin as a default replacement for Dagger. We wrote an interesting article comparing Kotlin vs Java if you want to take a look

Kodein

Similar to Koin, Kodein has more features and a bit more complex of an API.

Toothpick

While Koin and Kodein are more of a service locator, ToothPick is closer to Dagger because it uses annotations to provide compile-time code generation which results in better performance when needed.

Considerations for Large-scale Android Apps

Clean Architecture

When it comes to large scale apps, ViewModels and a Repository will simply not scale well, especially if multiple developers are working on the same app at once. In situations like these, Clean Architecture can provide a nice and robust way for large apps to scale. It can be even implemented with multi-module Android studio projects where each team can work almost independently of another team on the same app at once.

Layers

The core concept of Clean Architecture is Layers. Like an onion, an application will consist of layers that wrap around other layers. Each layer will have its own models and will be independent of the layers below. At the top, the Domain layer is where the business logic of the app resides and is independent of all other parts of the application like networking, Android, and database. 

Modules

Layers can be implemented as separate modules in the Android studio. Basic setup would be a presentation module for the Android framework classes like Activities, Fragments, and ViewModels. A data module can be used where the network layer is implemented like repositories and retrofit classes. Finally, a domain module fits in where all the business logic is implemented with the use of UseCase classes.

Final Thoughts

As we’ve discussed, there are many kinds of architecture for Android apps. Google’s primary recommendations support MVVM, making use of things like LiveData and ViewModels to address the two most common issues that Android apps face: lifecycle and rotation-change pitfalls. Proper separation of logic and behavior allows applications to be both flexible and easy to maintain.

Thankfully, Google provides lots of resources through its Jetpack guide to help developers get started. From there it’s easy to build out an architecture to support a project’s needs by making decisions on things like what to use for dependency injection. In doing so, it’s important to remember to keep things simple wherever possible.

For teams working on complex projects, Clean Architecture may prove to be less limiting. Its modularity can be beneficial when multiple developers are working on an application at once.

Originally published on Jan 11, 2021Last updated on Apr 13, 2023

Key Takeaways

What is Android architecture?

Android's architecture comprises a well-structured software stack tailored to accommodate the diverse demands of mobile devices. The Android software stack encompasses a Linux Kernel, an assortment of C/C++ libraries, a robust application framework, a runtime environment, and various applications. These components synergize to deliver a versatile and efficient platform, facilitating smooth app development and optimal performance for Android devices.

Which architecture is best for Android app development?

There is no one-size-fits-all architecture for Android app development, as the best architecture largely depends on the size, complexity, and requirements of the app being developed. However, some popular and widely-used architectural patterns include Model-View-ViewModel (MVVM), Model-View-Presenter (MVP), and Clean Architecture. MVVM and MVP are suitable for small to medium-sized apps, while Clean Architecture is a more robust solution for large-scale apps.

What are the types of Android architectures?

There are multiple architectural patterns for Android app development, including Model-View-Controller (MVC), Model-View-Presenter (MVP), Model-View-ViewModel (MVVM), and Clean Architecture. These patterns provide various levels of separation of concerns, making it easier to maintain, test, and scale apps based on their specific requirements and complexity.

Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam, just great content once a month.

 

Read Next

Browse Our Blog