Just like all software, Android apps should adhere to common architecture rules and patterns. Android apps that don’t follow the right architecture tend to become unmaintainable through cluttered Activities and Fragments lacking a consistent design or set of behaviors.
Given the importance of good architecture, how does one choose which to use for a project? In most cases, I like to recommend Google’s standard architecture for Android apps as a starting point, and then as the app grows more complex, concepts can be added.
So what does Google recommend as a starting point for an Android app? Well, it’s simple. Here are a couple of rules to follow:
- Be reactive
- Use ViewModels with LiveData
- Use a repository for data fetching and caching
But as we’ll see, there’s more to it than that. For example, I believe the addition of Dependency Injection is a must, and there are several options available. In this article, I’ll discuss the strengths and weaknesses of the common architectures for Android apps that are currently in use in order to help you decide the best approach for your next project.
What makes a good software architecture?
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 a platform:
Let’s explore each point in more depth.
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…
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.
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.
Popular architecture for Android apps
MVC (Model – View – Controller)
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.
MVP (Model – View – Presenter)
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.
MVVM (Model – View – ViewModel)
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.
MVI (Model – View – Intent)
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.
Google’s recommended architecture for Android apps
Out of all these options, what does Google actually recommend? MVVM. Let’s dive into the basic building blocks of an MVVM toolset.
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.
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.
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.
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 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.
What Google’s architecture for Android Apps does well
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.
Negatives of Google’s recommendations
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 recommendations?
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 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.
Similar to Koin, Kodein has more features and a bit more complex of an API.
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.
What about large scale apps?
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.
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.
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.
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.
Regardless of what architecture you’re considering using, Scalable Path has a wealth of Android developers ready to get started on your next project.