Android Architecture šŸ—

A Journey Looking For The Perfect Design

Philippe BOISNEY
ProAndroidDev

--

The Android landscape has so changed over the years, especially those two last years (thanks to Kotlin & Jetpack), and thatā€™s pretty a good thing.

Today, Google seems to finally give us a pretty clear direction of how we should start to design our Android apps.

In a few words: multi-modules & MVVM.

The purpose of this article is not to present in detail each concept (MVVM, Coroutines, LiveData, etcā€¦), but rather to share with you an overview of what could be a robust and scalable architecture in Android.

Disclaimers: This will be a long journeyā€¦āœˆļø

Table of Contents

1. Demonstration Project šŸ“±

This is an 8K GIF. To much power in it.

Because a picture (well actually a project) is worth a thousand words, I created a sample Android app to show you each part of this architecture:

I also tried to use the most recent and used libraries I could to have a realistic and complete sample:

2. Considering Multi-modules šŸ—„

If youā€™re a senior Android developer, youā€™re surely used to monolithic Android applications, where all your classes are inside one module divided into multiple packages.

Unfortunately, this approach tends to lead to high build times, difficulties to manage code in large teams and impossibility to reuse code across apps.

šŸ“š For more info about modularization, those articles rock:

In this part, I will show you how you could divide your app into multiple modules. This design is based on this diagram from Android documentation that I tried to respect as much as possible:

Translated into a multi-modules Android project, hereā€™s how this could be represented:

Letā€™s dive into each module compositionā€¦ šŸŒŠ

2.1. App module.

This module is responsible for almost nothing and is actually doing nearly nothing, except managing dependency injection and including the other modules.

Itā€™s the entry point of the app.

Because this module is the central point of our app, it will contain all the other modules:

Extract from build.gradle (app).

2.2 Data modules

Those modules are obviously about data and the way to retrieve it. Furthermore, each module is responsible for the creation of its objects by creating a DI Module (this ā€œmoduleā€ term is about dependency injection, donā€™t be confusedā€¦ šŸ˜…), used by the dependency injection framework (in this example, Koin).

Example of a DI Module created for the ā€œremote moduleā€.

In practice, those ā€œdata modulesā€ are placed by convention into a data folder. You will find for instance a remote module, local module and repository module.

ArchApp ā€œdataā€ modules. You can also find them here.

As you may have noticed, the thing pretty cool with modules is that you can easily link them with Gradle (and so reuse them !).

ā€œrepository moduleā€ depends on classes that are located within ā€œremoteā€ & ā€œlocalā€ modules.

2.3 Feature modules

Those modules represent your features (basically each screen of your app). Located by convention into the features folder, each feature module is totally independent and is essentially composed of:

  • One or multiple controllers (Fragment/Activity).
  • A single ViewModel for each controller.
ArchApp feature modules. You can also find it here.

Such as data modules, each feature module is responsible for creating its dependencies through a DI Module :

DI Module of the ā€œHomeā€ feature (source)

Finally, a feature module only has to include the repository module inside its Gradle file in order to access some data.

2.4 Navigation module

Thatā€™s the tough part, especially when you are in a multi-modules environment. Why? Because at one point, you will have to navigate between feature modules.

Feature modules are independents, and must NOT know about each other (neither about app module).

As far as I know, there is no official way to handle this properly. Instead, there are multiple and clever approaches to navigation in a multi-modules environment as you can see here or here (essentially based on Deep Link).

Personally, I choose the Navigation Component to handle this (I talk fully about it further in this article). In this way, the navigation module is only responsible to handle navigation in the application thanks to a powerful system of navigation graphs.

ArchApp ā€œnavigation moduleā€. You can also find it here.

In addition, navigation module depends on no other module, but is always included in feature modules.

However, note that this approach leads to some ā€œacceptableā€ limitations as we will see later.

2.5 Common & CommonTest modules

Those last two modules contain some ā€œcommonā€ classes potentially usable everywhere.

However, the common_test module is only used through Unit & Instrumented tests (included with Gradle using testImplementation and androidTestImplementation).

ArchApp ā€œcommon modulesā€. You can also find them here.

Of course, you are totally free to completely change this approach of multi-modules and adapt it to your needsā€¦ šŸ™‚

For your information, you can also find out other approaches about how developers are modularising their apps in this post.

3. Considering MVVM & Databinding šŸš€

Since Jetpack and the Architecture Components, a lot of things have changed in the Android ecosystem, in a good way.

Thanks to the ViewModel component, it became easy to store and manage UI-related data in a lifecycle conscious way. Also, the ViewModelclass allows data to survive configuration changes such as screen rotations! šŸŽ‰

And of course, ViewModel brings us naturally to the MVVM architecture.

šŸ“š For more info about MVVM, those resources rock:

In practice, hereā€™s how it looks like in a multi-modules Android project:

Interactions in an MVVM architecture

As you can see in this type of architecture, we tend to use the ā€œcompositionā€ principle a lot in order to reuse each class more easily. That is one of the main reasons you need a dependency injection framework (like Koin or Dagger2) to handle this more properly.

3.1 MVVM: Model

The ā€œModel Componentā€ represents the data and the business logic. Because we are in a multi-modules environment, itā€™s pretty easy to include the ā€œmodelsā€ we need (for example the repository module).

Also, because we need sometimes to adapt/modify raw data fetched from the repository module , itā€™s a good practice to create a Use Case class between the Repository and the ViewModel. Thatā€™s what Plaid app does for example.

The Use Case principle is a part of the Clean Architecture. I really loved this article that clearly talks about it šŸ‘Œ

Letā€™s take for example the ā€œHomeā€ feature module.

Extract from GetTopUsersUseCase

As you can see, this ā€œUse Caseā€ has a UserRepository injected directly in its constructor. Iā€™m also using the invoke operator function to make sure this use case contains only a single action.

Extract from UserRepository

Then, the previous Use Case will be used directly by the ViewModel of the feature.

3.2 MVVM: ViewModel

The ā€œViewModelā€ component represents the heart of your feature/screen.

Ideally, when you look at the code inside a ViewModel, you should be able to understand and have a clear vision of :

  • šŸ’Ŗ ALL the actions a user can make in your feature/screen
  • šŸ”® What kind of data the view (UI) will show

Letā€™s take a look at the ā€œHomeā€ feature ViewModel :

Extract from HomeViewModel

As you can easily guess when reading this class, the ā€œHomeā€ feature will :

  • šŸ”® Show to the view a Resource<List<User>> through the public users LiveData. In this way, UI will be able to observe the state of the data (loading, error or success) and the actual data (List<User>)
  • šŸ’Ŗ Offer two actions to the user which use our app: userClicksOnItem() and userRefreshesItems()

Note: In this sample, Iā€™m using the MediatorLiveData to properly handle the refreshing of the users LiveData (pull to refresh).

By the way, I highly recommend you to read this article to know whatā€™s the best practices & anti-patterns when it comes to using a ViewModel.

3.3 MVVM: View

Through the MVVM architecture, the ā€œViewā€ component has simply the responsibility to observe the LiveData and react to its changes.

In Android, we will use data binding to achieve this behavior. In this way, our controllers (Fragment/Activity) will be as light as possible!

Extract from HomeFragment

Pretty light, isnā€™t it? As you may know with data binding, the most ā€œinterestingā€ part is located within the XML layout :

Extract from fragment_home.xml

Thanks to data binding, we basically tell the layout to use our ViewModel and its LiveData in order to print their data to the screen or proceed to some user actions.

If you need some custom binding behaviors, you can create some BindingAdapter like this way :

Extract from HomeBinding

Thanks to this approach, each concern regardless of UI is clearly separated. No need of findViewById, Butterknife or Kotlin Android Extensions anymore (and it seems to be the Google point of view).

4. Considering Navigation Component ā›µļø

As I told you previously, navigation in an Android multi-modules environment is pretty tough and complicated.

Recently, Google released Navigation Component which really helps us to build complex navigation within our apps, as simply as it should be.

šŸ“š For more info about Navigation Component, those resources rock:

In this way, I tried to find out how we could implement it in a multi-modules project. Android documentation seems to tell us that itā€™s possible:

You should include the top-level graph in your main app module and have a Gradle reference for any modules containing navigation that you need to reference.

Why not! However, in practice, I didnā€™t want to centralize navigation in the app module. So I chose to implement David VĆ”vraā€™s approach and create a navigation module that will handle all the navigation process of my app.

This solution comes with two ā€œacceptable drawbacksā€ as explained pretty well by David :

1. Since ā€˜navigationā€™ module doesnā€™t have dependency on features, Fragment classes are red in Android Studio. You donā€™t have auto-complete for Fragment classes, but it compiles without any problems.

2. There is a bug which prevents generating intent filters for deep links. Itā€™s already assigned, so hopefully it will be fixed in the final release.

Red auto-complete in Android Studio

Despite those drawbacks, we can perfectly use the Navigation Component inside our app and enjoy all its awesome features like:

  • SafeArgs: The Navigation Architecture Component has a Gradle plugin called Safe Args that generates simple object and builder classes for type-safe access to arguments specified for destinations and actions.
Using SafeArgs feature
  • Navigation UI: NavigationUI contains methods that automatically update content in your top app bar as users navigate through your app. For example, NavigationUI uses the destination labels from your navigation graph to keep the title of the top app bar up-to-date. Pretty cool to easily handle the ā€œUp buttonā€.
  • Easy testable: You will use FragmentScenario for testing the contents of your fragments in isolation. This allows you to verify a fragment's state and interactions in both unit and instrumentation tests.
Test using FragmentScenario. Extract from HomeInstrumentedTests.
  • Easy animations: The Navigation component lets you add both property and view animations to actions. As you may have seen in the demo project, it became so easy to add custom transitions or using Shared Element Transitions between destinations.

5. Considering Unit & Instrumented Tests šŸš¦

In my opinion, a strong and scalable architecture should mean a full and easy testable environment. In a multi-modules architecture, testing becomes very accessible (because decoupled).

Letā€™s take a look at the tests of data modules :

Extract from UserServiceTest (remote module)
Extract from UserDaoTest (local module)
Extract from UserRepositoryTest (repository module)

As you can see, each module (remote, local & repository) are fully tested. Among the libraries I used, you will find Mockk (for mocking) and MockWebServer (to mock a fake web server), nothing more.

Each feature module is also fully tested (unit & instrumented tests)! Letā€™s see what it looks like with the home feature :

Instrumented tests for the ā€œHomeā€ feature (source)
Unit tests for the ā€œHomeā€ feature (source)

Those tests were actually pretty simple and swift to write, especially because each component is decoupled so it becomes really easy to properly isolate and test each one of themā€¦ šŸŽ‰

Proper testing is in my opinion, one of the most important parts when you want to design a scalable and performant application. However, sometimes, this part may be very hard, especially if your app is not properly designed and architectured.

Today, thanks to Architecture Components & Jetpack, building robust, testable, and maintainable Android app starts to become less painful and more supervised/standardized by Google.

If you are starting a new Android project, I highly recommend you to consider multi-modules and MVVM architecture. As you may have seen, itā€™s really worth it.

--

--