Recently, I was in requirement for developing a Multi Module app in Android and I was going through this great lecture on it. This article is the summarization of the Android Multi Modular talk from Google IO 2019.
In this first article, we will briefly discuss about the approaches for Multi Modular app creation in Android. In the following article, we will look into the example app.
Advantages of a Multi Module app in Android
- Developers can work on certain sections of applications without slowing down other developers.
- Maintainability. All the files can be maintained in respective modules where it will be easier to find what we are looking out for.
- Incremental Compilation. If we modify a file, the modularised apps compile faster than the monolithic apps. Below screenshot shows the difference between the invalidation of files for monolithic app vs modularised app.
- CI / CD will run faster. In the below example, If we use Incremental CI and If we made a Change in module 5, then the only tests CI pipeline can run are the tests present in Module 5 and the tests present in App Module because app module depends on all the modules.
- With App Bundles and Dynamic Delivery, we can significantly reduce apk size and therefore we can greatly experiment the different features which can take up large space.
Types of Modularisation
We will discuss about gradle library modules and dynamic feature modules here.
How we can Modularise?
We can modularise either by feature or either by layer. Let’s discuss about them in detail.
Modularisation by Features
Feature modularization consist of a base app – which is an android application and modules each with respective functionality – each extending android library. The app modules depend on these modules. Hence it implements those modules. Implementation of module in app gives the access to code and resources from that module to the app. Few Modules can be dynamic feature Modules.
Dynamic Feature Modules
Dynamic feature modules are the modules which can be used for on-demand code delivery which can be downloaded later. These modules will depend on the App module. We need to declare them in our app module’s build.gradle as dynamic feature modules.
The main restriction of feature modules in the app is – the app module cannot depend on dynamic feature modules. So in the above example, app module doesn’t depend on module 2 and module 3 as they are declared as dynamic modules.
App depends on Module 1 (Library Module). Hence in app’s build.gradle, we will add:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
implementation project(':module1') |
The other modules – module 2 and module 3 will implement app module as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
implementation project(':app') |
The app will declare them as dynamic feature modules as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dynamicFeatures = [':module2', ':module3'] |
Few dynamic features can be installed on-demand and few while downloading the app itself. These can be differentiated by using onDemand parameter.
If we enable the onDemand, we need to provide the module that can be downloaded via play store.
Core Module
Shared code and resources will be the part of core module.
The best feature of on-demand is that if there are third party libraries which the on-demand module requires, we can download them later saving the lot of space that requires the third party app to download.
Related Links
How to decide what feature can be declared as dynamic module?
Mostly users uses 20% of our app. We can deliver the features such as Paid features, features which require space but can not used frequently at a later point of time. Often small features can be placed in library as they are small and user shouldn’t wait for long for them to download dynamically.
Eg: Search and About features can be library modules.
Layer Modularization
The following image is the example of Layer Modularization where we are declaring different layers color coded into different modules.
API vs Implementation
Suppose Module A has API dependence on Module B, it means all the public functionality by Module B is accessible by Module A where as when A has Implementation dependence on B, modules that depend on A cannot access B’s functionality.
Advantage of this kind of modularisation is testing. We can easily create fake implementation for repositories where the team member working on the repositories layer knows exactly how the data needs to be mocked.
Feature modularisation vs layer modularization
Feature modularisation brings in the encapsulation and possibility of on-demand delivery.
Layer modularisation brings in isolation and structure to our app.
Challenges and solutions to work with Dynamic Feature Modules (DFM)
There are few challenges involved inside dynamic feature modules. Let us discuss about them.
Navigation
Let us think our about feature is inside DFM and we are in our app’s main activity. Now we want to launch the AboutActivity present in DFM where we do not have access to it. To overcome this we need to declare and pass the component name as follows:
If we are using fragment then we need to add it to proguard exclusions also for it to work properly. For fragments, here is the code snippet:
While these are harcoded strings are not a good solution, google team promises to fix them in the next releases.
Communicate between modules
Let us take a scenario to properly analyse the problem between moSuppose we need to search from two different modules – stories and users. Suppose search is a different module. We need to use search module to perform search on posts and users module. The way we do is to create an interface in core module – DataSource, which these two modules depend on, and implementing the interface in both modules. This gives two classes – StoriesDataSource and UsersDataSource – both implementing DataSource interface.
But to instantiate these data sources, we need to instantiate the following too:
The above classes such as service, sharedpreferences, etc. needs to be instantiated for the data sources to be instantiated.
In core, we will create a DataSourceProvider interface that will contain getDataSource() function.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface DataSourceProvider { | |
fun getDataSource(context:Context):DataSource | |
} |
Then we will create StoriesDataSourceProvider that will implement DataSourceProvider.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class StoriesDataSourceProvider : DataSourceProvider { | |
override fun getDataSource(context:Context) : DataSource { | |
//build dependencies | |
return datasource | |
} | |
} |
In search, we will provide the set of all data sources. We will check if feature is installed. If it is, we will create the new instance using reflection or service loader as DataSourceProvider as shown in the image below:
In the above example, if we use service loader, the provider will be like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
… | |
val providers = ServiceLoader.load(DataSourceProvider::class.java, null) | |
val dataSources = providers.map { | |
it.getDataSource(activity) | |
}.toSet() | |
… |
Here is the link to google samples to checkout both of the above versions:
https://github.com/googlesamples/android-dynamic-code-loading
DataSources using Dagger
If we are using Dagger, we will be using component – CoreComponent which we created in core module.
This good when dynamic modules are already installed. If they aren’t installed, we will use LiveData<Set<DataSource>>.
We can register listeners to react to installation of new modules.
Working with Databases in Multi module app in Android
When we modularise an app, we face problem about where to keep our database. While there is no specific correct method to do so, one of the implementation is using a single common database for the entire app. The other is creating a database for core common functionality and then separate for feature modules.
Pros / cons of a Single Database
Pros:
- Easy to maintain database connection since we need to open one db connection only.
- Easy to share tables.
Cons:
- No isolation between modules therefore we need to rename these tables to not conflict across modules.
- No specific entities are in shared domain. We need to put all entities and daos into core module that ships with app.
Pros / cons of a One Database / Module
Pros:
- Perfect isolation between modules
Cons:
- DB connection maintanence. If we want to write a queries across modules, it is complex.
The google devs promise the multi modular aproach support using Room database, but in future.
Reference:
In the next tutorial, we will be creating a Multi module App in Android.