Dagger and Multi-module Traveloka Android App
Authors: Alifa Nurani Putri (Android Software Engineer), Yusuf Cahyo Nugroho (Android Software Engineer)
Dagger is a popular dependency injection framework for Android development. If you’ve heard about it and want to start immediately implementing it, there are plenty of articles out there that will help you to do that. Instead, what we want to share with you today is our journey at Traveloka in implementing Dagger while our Android app continues to scale up, specifically in facing the adoption, problems, and refactors along the way which hopefully, you will be able to pick up invaluable lessons from.
Before we start, please be advised that this article focuses mainly on Dagger implementation for a large-scale multi-module app (Traveloka app is now delivering 25+ products). It’s best suited for those who have had basic understanding of Android development, dependency injection, and Dagger 2 (Component, Subcomponent, Module, Scope).
SPOILER: From our journey, you can get tips to decrease build time and cold-start time, make a clearer dependency, and get the feasibility of Dynamic Feature Module.
In the Beginning
It was the end of 2016 when we decided to adopt Dagger. At that time, Traveloka app was quite stable with millions of users already using it and we didn’t want to take the risk of destabilizing it, despite having the knowledge of how powerful Dagger is. So, we began using Dagger only in Application
class (TravelokaApplication
).
We created a single Dagger component named AppComponent
to hold Context
, Application
, and providers (in our architecture, provider is the layer responsible for reading / writing data to be used by the presenter). The block of code below describes how simple it is to start our Dagger adoption.
A few months into 2017, Traveloka has launched a number of new products. At that time, we have proved that our Dagger implementation in the Application
class (TravelokaApplication
) didn’t cause any problem. We then started implementing Dagger as well into product’s codes as Subcomponent of AppComponent. Each product also has its own provider module, as shown by the diagram below.
Our starting adoption of Dagger ran smoothly. It was nice to know you, Dagger.
We Started Facing Problems
All is well until we realized that our single Dagger component — AppComponent
— came to be a God Object. It had 82 Dagger Modules and 36 Subcomponents. Moreover, the generated file DaggerAppComponent
had 17705 lines of code.
Back in the days, when we decided to use a single Dagger component, we had predicted that it could get sticky in the future when the app keeps scaling up. But, we had another reason at that time to mute our prediction. In several cases, we needed singleton for holding state across activities. Yeah, please don’t judge us :D. We knew it was a bad practice but didn’t think too much about the drawbacks in advance.
Without further ado, here are the top 3 problems we faced;
1. AppComponent Holds Excessive Objects
Using a single Dagger component (AppComponent
) means all injected objects will still be kept alive as long as the application is alive whether the object is still being used or not. To give you a picture, let’s say a user opens the Traveloka app and does several actions: 1) Login 2) Search for a flight 3) Search for a hotel 4) Search for a train 5) Make a train booking 6) Go to the payment page. Objects that are injected in Flight, Hotel, and Train search pages are still kept alive until the 6th action, even though they’re not used anymore.
Another drawback is cold-start time. Dagger will create DoubleCheck object for each object that belongs to the component (AppComponent
) on initialization (application OnCreate
). The more objects contained in AppComponent
, the more time is needed to initialize Dagger, which will in turn, cause a longer cold-start time.
2. Diamond-shaped Dependency and Build Time Issue
Diamond-shaped Dependency describes the module dependency in the Traveloka app. Application module (app
) depends on all product modules and each product module depends on core
. To clarify, each product module may contain more than 1 Android library module, such as the no-op module. Other kind of modules will be introduced later in this article.
This diamond-shaped dependency causes a build time problem in incremental build case. Whenever public Java-based code changes in any product module, the app
also needs to be rebuilt, which may take additional 2+ minutes.
So, what is the relationship of diamond-shaped dependency with Dagger? AppComponent
, our single Dagger component that holds all of the dependencies, can only be located in the app
for it knows — has reference to — everything. By continuously using a single Dagger component, we keep that diamond-shaped dependency alive and won’t be able to solve the incremental build time issue.
Breaking diamond-shaped will increase build time
3. Dis-utilization of the Upcoming Dynamic Feature Module
Dynamic Feature Module (DFM) requires a feature module (the same definition as product module in Traveloka case), which depends on the application module, not vice versa. By maintaining the diamond-shaped dependency, we won’t be able to try Dynamic Feature Module.
The further we are in this journey, we started to believe that a good library with miscalculated adoption will eventually produce drawbacks that could dwarf its benefits.
The Refactor Journey
Finding the best Dagger implementation is not an easy refactor story for us. Let me take you through the journey step-by-step. It might be a bit long. Therefore, my suggestion is grab a cup of coffee or tea and let’s enjoy the journey together. :)
First things first, we should agree that this refactor aims to make our AppComponent
smaller. By doing this refactor, we expect a faster build time, a lower cold-start time, and open the feasibility to implement Dynamic Feature Module.
Convert Subcomponent into Component
I’m sure that some of you are questioning, “Why does it sound so complicated? Why not just convert each product’s Subcomponent into independent Component?” Our answer, “Nice, you get the point!”
We agree on the idea of independent and internal Dagger component, where it only holds objects that are used by its own product module. Product component (e.g. FlightComponent
, HotelComponent
) can live in screen levels like Activity and Fragment. You can also use Scope, as we use @ScreenScope
to annotate this case.
Use Scope as needed. Otherwise, object will always be recreated when it’s needed.
Unfortunately, in Traveloka case, implementing modularization is not easy because of Cross-Sell Modularization. To give you an example, Traveloka offers cross-selling in Flight together with Hotel and Car Rental that can make Hotel and Car Rental views to appear in the Flight screen.
If we only convert Subcomponent
to Component
, then Flight will depend on Hotel and Car Rental for cross-selling. But we want to avoid that because we are actually deviating away from modularization; a concept of breaking system into multiple degrees of interdependence and cross-independence. Flight shouldn’t know about Hotel implementation (and vice versa), but should only be encapsulated with a contract. By defining such dependency, it could also potentially produce circular dependency whereby, for example, Hotel needs to cross-sell Flight on its page and Flight in turns needs to cross-sell Hotel on its page.
Don’t make a product module depends on another product module. Each has separate business model and the dependency should be encapsulated with contract.
Core-Layer Module to the Rescue?
There was an article, which suggested creating a core-layer module to place shared objects as a contract. We have used this solution and named it all-api
module. In Traveloka case, almost all product modules need to depend on all-api
. But, this solution could be problematic as well when all-api
is overloaded for It will cause a longer build time and make the modularization unclear.
Introducing Product API Module
Our next attempt was to separate all-api
into individual product api
module. A product module should only depend on some specific product api
modules as needed.
Therefore, we created a Dagger component for each api
module. It will be the responsibility of an api
component to hold the dependencies needed by other product modules. To piggyback on the previous Flight, Hotel, and Car Rental example, the diagram below describes the dependency pattern of using product api.
api
ModuleFor example, a service named HotelSearchService
will be used by Flight to search for Hotel inside Flight’s cross-selling screen. We need to create an interface for HotelSearchService
. Then, HotelApiComponent
(Dagger component for hotel-api
) will be used to expose HotelSearchService
. Flight only needs to depend on hotel-api
and HotelSearchService
can then be injected.
Problem: hotel-api
module only has the interface of HotelSearchService
. The implementation is still located in hotel
module. How to bind that interface to its implementation?
Solution: Since only the app
module knows about everything, it should be the one responsible to do the binding task during the app’s initialization. It will trigger every product module to register the implementation to the core
module. Back to the case, by the time flight
module needs HotelSearchService
, the implementation is already bound and ready to be used.
Introducing Product Base
Module
Another problem: some objects need to be present immediately after the application has finished with initialization.
Solution: We created another type of module named base
. Those immediately-needed objects will be held by product base
Subcomponent.
For example, we use it for our deeplink service where, every module has its own deeplink service that should be present immediately and must be alive as long as the application is alive. We need to register deeplink services to a collection (e.g. Set
, Map
) using Multibindings during app
initialization. Product base
module (a Dagger module) is responsible to bind that. The collection itself will be held by AppComponent
.
Challenge: At Traveloka, every product team has individual priorities. The central Android team can’t enforce all product engineers to refactor at the same time. Therefore, base
product module is created to temporarily handle this challenge by resolving dependencies from product modules that haven’t been refactored yet (still don’t have api
component).
For example, the graph below shows an example where FlightProvider
needs PaymentService
. But, in this case, payment
module hasn’t been refactored. This is solved by creating FlightBaseSubComponent
as a Subcomponent
of AppComponent
, so it can expose PaymentService
.
Bottleneck and dagger-bridge Introduction
I have mentioned before that we wanted to break the diamond-shaped dependency for it will make a faster build time and open the feasibility of adopting Dynamic Feature Module (DFM). So we did. app
module only needs to depend on product api
and product base
module if needed. We can decouple product module from the app
module and try convert product module as a feature module of DFM.
Problem: We faced DFM issue that cause a bigger application size when implementing DFM. Consequently, we decided to postpone the adoption of DFM.
Then, is it possible to at least make the build time faster for now?
Solution: We introduced a new layer module called dagger-bridge
, which is actually an empty module. What we want to achieve is to pseudo-decouple product module from app module. Currently, only dagger-bridge
that needs to be rebuilt when any code is changed in the product module (incremental build).
The Idea in Brief
In a simple way, every product module could have 3 kinds of modules: Internal, api
, and base
. Internal is mandatory (the main module), api
will be used when other module needs dependency from it, and base
will be used when an object should be present immediately after app initialization. The diagram below illustrates the relationship among those modules followed by the associated code examples afterwards that explain our Dagger refactor implementation.
Internal Module
Flight component is located in flight
module and annotated with @ScreenScope
. It depends on HotelApiComponent
and FlightBaseSubComponent
.
Api Module Explanation
HotelApiComponent
contract is located in hotel-api
module, which is used to expose HotelSearchService
needed by flight
for cross-selling .The implementation of HotelApiComponent
is located in hotel
module, which is used to bind interface to implementation.
Base Module Explanation
FlightBaseSubComponent
is located in flight-base
module, which is used to expose PaymentService
from AppComponent
(in this example, payment
module hasn’t been refactored)
In addition, FlightBaseModule
, located in flight-base
module, is used to provide objects from flight module that are needed immediately after application initialization.
Proof
What do you think about our new Dagger implementation? Will you believe if it does make a better impact? Let our benchmark’s results speak for themselves.
Initialize Time Comparison
For a sample case, we refactored 1 out of 25+ product modules. We proved that the time needed to initialize dagger had decreased from 357ms to 219ms on average from several experiments.
Incremental Build Time Comparison
This scenario was used to verify that the average incremental build time did decrement after the refactor (with the use of dagger-bridge
). We compared the build time by changing a public method signature before and after refactoring. We also used two product modules for the sample.
We ran each build in our local machine (MBP 2017 15" i7 16GB 500GB SSD) and used Gradle Enterprise to analyze. In short, using dagger-bridge
module will negate several tasks from being executed if the changes were only present in the product module. Those several tasks were :app:processDebugAnnotationsWithJavac
and :app:compileDebugJavaWithJavac
.
What’s Next?
Currently, we are still midway through the refactoring process. As you are reading this article, 9 out of 25+ product modules in Traveloka app have already completed the refactor.
After all product modules have been refactored, we want to prove that our new Dagger implementation is Dynamic Feature Module-friendly. Bringing a product module to the lowermost part of the dependency graph (depends on app
, not vice versa) will hypothetically make another decrement in build time. Hopefully, we can prove it in due time and share the story with you again next time :)
Anyway, this experience is best for our case and therefore, It could be different on your end. Share your potentially different experience or better approach(es) with us with your comments below.