Writing a modular project on Android
--
When we create a new project on Android Studio, it gives us one module, the app
module. This is where a majority of us write our entire application. Each click on the run
button fires a gradle build on our entire module and all the files in it are checked for changes. This is why gradle builds can take 10’s of minutes for larger applications and slow down developer output.
To solve this, complex applications like Uber decided to modularize their applications and have gained a lot from it. Following are some of the advantages of having a modular project
- Faster gradle builds
- Re-usability of common functionality across applications / modules
- Easily pluggable into Instant apps
- Better team work, as one person can have the sole responsibility of a module
- Smoother git flows
Due to the above advantages, when I started off with the Posts application, I kept it in mind to use a modular approach from day one. The Android team has provided us with a few tools for it, but I did hit some roadblocks. Following are some of my learnings:
How do I split my modules?
Your application has sets of flows, for example, Google play has the application details flow, which contains the summary, description details, screen-shot and reviews activities.
All of these can fall under the same module — app-details
.
Your application can contain multiple such flow modules like authentication
, settings
, on-boarding
, etc. There are also modules which do not need a UI element to be present like — notifications
,analytics
, first-fetch
etc. These modules contain the activities, viewmodels, repositories, entities and dependency injections pertaining to that flow.
But there are always some common functionalities and utilities these modules would want to share. This is why you need a core
module.
What is in the Core Module
?
The core
module is a simple library module in your project. The core module can, among other things,
- Provide global dependencies to your dependency injection framework like Retrofit, SharedPreferences, etc.
- Contain utility classes and extension functions
- Contain global classes and callbacks
- Initiate libraries like Firebase Analytics, Crashlytics, LeakCanary, Stetho, etc in the application class.
How do I use 3rd party libraries?
One of the main responsibilities of the core
module is to also provide external dependencies to your feature
modules. This makes it easy to share the same version of a library among all your features. Just mark the dependencies with api
in your core module and all your dependent feature
modules would be able to receive them.
There is a possibility of a dependency only being useful in feature-a
module but not in feature-b
, both of which depend on core
. In that case too, I would recommend to define your dependency in the core with api
as proguard will take care of not including it into the feature-b
instant app.
How do I use Room?
This one confused me for the longest time. We would want to define our database into the core
module as it is a common functionality our application will want to share. For Room to work, you need a database file with all the entity classes mentioned into it.
But, as mentioned above, our entity classes are defined in the dependent feature
modules and the core
module cannot access them. This is where I hit a roadblock, and after some thought did the best thing you can do, ask Yigit for help.
Yigit clarified that you will have to create a new db file into each feature
module and have a database per module.
This has some advantages:
- Migrations are modularized
- Instant apps contain only those tables they need
- Queries will be faster
Cons:
- Cross module data relations won’t be possible
Note: Do not forget to add the following dependencies into your feature
modules for Room’s annotations to work
How do I use Dagger 2?
The same problem as Room was hit with Dagger too. My application class in the core
module would not be able to access and initialize my feature
module components. This is the perfect use-case for dependent components.
Your core component mentions the dependencies it wants to expose to the dependent components
Your module components define the CoreComponent
as a dependency and use the passed on dependencies
Where do I initialize my components?
I created a singleton holder for all the components of my feature. This holder is used to create, maintain and destroy my component instances.
Note: Do not forget to add the following dependencies into your feature
modules for Dagger’s annotation process to work
Conclusion
Although there are some tricky parts to convert your monolithic application into modules, some of which I tried to solve above, the advantages are profound. If you hit any roadblock with your modules, feel free to mention them below and we may work together on a solution.
Thank you!