But in cases where our applications may grow, or we may want to take advantage of new distribution features (Instant apps, app bundles) from Google, or even just create a clear separation of concerns to make our project easier to work with— modularizing our applications can help us to achieve all of these things. And because some of these newer tools from Google are aimed at modularized applications, who knows what may become available in the future for applications that are structured this way.
Recently I’ve playing with Instant Apps as well as the new App Bundles, which means there are now more ways to modularize our projects — and knowing a little bit more about these can help us to know the right ways to configure our projects. Maybe you want to modularize your application, or maybe you want to get a clearer understanding on the differences between the different modules that we can create for our project – in this post we’re going to take a quick look at the different kinds of modules we can create and what these can do for our android applications.
We’ll start here as it’s likely a module that you’ve already encountered or used at some point during your development career. And if not, then it’s the most important place to start! This is essentially the main module of your application and will often be under the app module, using the application plugin in the modules build.gradle file to signify this:
apply plugin: 'com.android.application'
Now if you’re building a single module application then this will be the only module-type plugin that you will be using throughout your project. In single module projects, this base module will contain all of your applications responsibilities — be in User Interface, Networking, Cache, Data operations — you name it and it will be there.
If you’re building a multi-module application, then the application module will still be the installed module, but the way this application module operates will vary depend on what it is you are building:
- If your multi-module project simply abstracts out data responsibilities of your application into data modules, then the application module will operate in the same way that you have worked with it in single-application modules but just with a clearer seperation of concerns.
- If your multi-module project is supporting dynamic feature modules then this application module will behave in pretty much the same way as the single module application . The only difference is that the module will need to define the module references for the dynamic features that are to be supporting for the application instance.
- If your multi-module project is for instant apps then this application module will simply be used to define the feature-module dependencies for the instant app. This is because when an instant app is being built, this application module does not contain any application code as its responsibility is just consume the feature modules which are declared.
You can essentially use these modules to separate out related areas of your project so that they are decoupled from the main module of your project. There’s not really a coined term for these but lets call them core modules.
For example, let’s say you have a project that supports both Android wear and Android Phones, you might have a module for the Wear UI layer, Phone UI layer followed by a core module. This core module will contain all of the shared business logic, data operations, caching etc that is used by both of the UI modules. In this case, you are promoting code re-use as all of these aspects can be shared between the two UI modules.
Re-use isn’t the only reason as to why you might want to create this kind of module though. Separating core logic can also help to create a clear separation of concerns that makes your code easier to understand, test and maintain across your team — you may also benefit from build time improvements in some cases.
So for example, you may have a large application with a complex data layer that communicates with both a remote and cache data source. In cases like this, separating out these parts of logic into their own responsibility-specific modules allows you to achieve these things. This could result in us having a data module (to handle the fetching of data and data source), remote module (to handle remote data source communication) and cache module (to handle local data persistence) alongside a presentation module to handle the user-facing parts of our application.
There are no strict guidelines as to what should be in these core modules. My advice would be to not move stuff out into module for the sake of it, but if you feel there will be some benefit for your team (like the ones mentioned above) then moving a responsibility into a module can help you to achieve some of the benefits of modularization.
Core modules that contain android framework references will need to use the library plugin:
apply plugin: 'com.android.library'
Whilst on the other hand, core modules that do not reference the android framework can simply be pure kotlin / java modules:
apply plugin: 'kotlin'
Core modules may often be used to move chunks of shared logic or independent responsibilities, but sometimes we may want to abstract some third-party responsibility out of our application. This can often be a good idea so that our application will not be tightly coupled to a specific implementation of something— in this situation we’d simply communicate via an interface between the app and abstraction module, allowing us to easily switch out the implementation for another if required. Therefore if some library becomes deprecated, a service shuts down or for whatever reason you need to change the implementation then you can do so with minimal interruptions to your code base.
In these cases, your abstraction modules will either use the android library plugin or simply be a pure kotlin / java module (depending on the library dependencies) that we previously looked at.
As well as this approach to splitting out back-end related tasks into core modules, you can do the same for user facing features within your app — we call these feature modules. These feature modules are going to contain specific features of our application which can help us to again decouple different responsibilities to achieve the same benefits as we’ve previously looked at. Now, the way that these are defined will depend on the kind of multi-project application that you are building, so let’s take a look at the different scenarios:
In a ‘standard’ application where you are not using instant apps or dynamic delivery, you may still want to split individual features out into modules to achieve some of the benefits from modularization, whilst also future-proofing yourself incase your app moves in an instant/dynamic direction along the line. In these cases you will simply package up a feature into an android library module and add it as a dependency to your application module.
apply plugin: 'com.android.library'
In cases where you are not supporting instant apps, or not ready (or need) to support dynamic delivery yet, featuring by library modules can still help you to decouple your codebase and achieve other benefits of modularization. In these cases you can simply declare modules as library modules using the library plugin and add them as a dependency to your application module.
Instant support application
On the other hand, if your app supports Play Instant then you will need to use the feature plugin for your desired feature module:
apply plugin: ‘com.android.feature’
Each feature module will need to have a reference to your base module (in this diagram, the app module) — the feature module in this case will only contain the required code for the implementation of that feature, any code that is required from the core of your app will be obtained from the base module reference.
Dynamic support application
These kind of modules can be used to decouple specific features and resources of your application into their own module, to then be used as part of the new App Bundle format which allows us to dynamically deliver individual features to users. This means we can reduce the initial install size of our applications, instead allowing users to download specific features at run-time as and when they are required.
Just like the instant app focused feature module, these dynamic-feature modules will contain pure feature implementations whilst still having a reference to the base module of the application. The only difference within this module is that the manifest file contained within it has some additional information regarding the distribution of this module.
We can define a module as a dynamic feature module by using the provided plugin below:
apply plugin: 'com.android.dynamic-feature'
Note: In future app bundles will provide support for instant apps.
Instant App Module
Last but not least, if you’re currently wanting to support instant apps then your application will need to be provide an instant module.
The contents of this module will be pretty empty most of the time — this is because the responsibility of the Instant Module is to simply build the instant app when request. Therefore, its sole purpose is to consume the feature modules that have been referenced as a dependency. When defining an instant module you will need to use the instant app plugin in your modules build.gradle file:
apply plugin: ‘com.android.instantapp’
The aim of this article was to provide a brief overview of the different modules that are available for android projects, as well as plant some seeds as to how you could approach modularization within your app. If you have any questions about modularization then please do reach out 🙂
Thanks to Donn Felker & Annyce Davis for the proof reading on this post 🙌🏻