Effective DI with
Multi-Modular Architecture
Adit Lal
@aditlal
Gojek is a Super App
We have 18+ products from food tech to fin-tech
to hyper local delivery and massage services
$6.3B
GoPay
16.5m KM
Ride Sharing
Solve DI with
Multi-Modular codebase
Dependency Graphs
Monolith
app
common auth
Expensive
Object core
What could possibly go wrong?
Modularise?
First rule
You don’t need to modularise
If you don’t have to
Feature modules
Feature A Feature B
app
Expensive
Object core
Thoughts on modularisation
• Separation of concerns
• Build performance
• Dynamic features ?
Thoughts on modularisation
Feature module
• Owned by single team ? Set of team members
• Encapsulates single feature
• Single entry point
• Smaller is better
• Cannot depend on other feature modules
Thoughts on modularisation
App module
• No feature-specific code
• No infrastructure-specific code.
• Creates the dagger component
How?
• First , move all code and resource into a core module
• Identify features that can be in independent module.
• Move related code and its resources to these feature modules.
Thoughts on modularisation
• First , move all code and resource into a core module
• Identify features that can be in independent module.
• Move related code and its resources to these feature modules.
Vertical dependencies should only have one direction.
Ownership?
app
Feature n-2 Feature n-1 Feature n Feature n+1 Feature n+2 Feature z
… … … … …
common auth
core
Ownership?
app
Feature n-2 Feature n-1 Feature n Feature n+1 Feature n+2 Feature z
… … … … …
common auth
core
Ownership?
IoC Principle
Inversion
of control
Dependency
Inversion
Dependency
Injection
IoC Principle
• Declare Components that can be reused.
• High level modules should not depend on low level modules
• Build abstractions so any implementation can be easily changed with
new one.
• Dependencies can be injected into components.
Should I do Modularise to use Dagger?
Don’t do it for Dagger , do it for yourself
Dagger and modular code?
• Faster build time ( with & w/o dagger)
• Better separation and encapsulation of your code.
• Access to Modularised Delivery.
• Time spent today to modularise is time you are saving tomorrow.
Scoping w/o Modules
App
Scoping w/o Modules
App
Expensive
Object
MainComponent
MainModule
Scoping w/o Modules
App
Feature 1
SubF1Component
dep
end
s
F1Module
Expensive
depends Object
MainComponent
MainModule
Scoping
App Feature 2
SubF2Component
Feature 1
SubF1Component F2Module
dep
end
s
F1Module en ds
d s dep
de
p en
Expensive
depends Object
MainComponent
MainModule
Circular … what … no
Playing Jenga
Scoping
App
Feature 1
MainComponent depen
ds
SubF1Component
s
p end
de
Core
Expensive
Object
Scoping - KISS (keep it simple stupid)
App
Feature 1
MainComponent
F1Component
ds
depends p en
de
Core Feature 2
Expensive
CoreComponent
Object depends
F2Component
Vertical dependencies should only have one direction.
Disclaimer
Core
@Module
class CoreModule {
@Provides
@Singleton
fun provideExpObj(): ExpensiveObject = ExpensiveObject()
}
Core
@Module
class CoreModule {
@Provides
@Singleton
fun provideExpObj(): ExpensiveObject = ExpensiveObject()
}
Core
@Component(modules = [CoreModule::class])
@Singleton
interface CoreComponent {
@[Link] interface Builder {
fun build(): CoreComponent
}
fun provideExpensiveObject(): ExpensiveObject
}
Core
@Component(modules = [CoreModule::class])
@Singleton
interface CoreComponent {
@[Link] interface Builder {
fun build(): CoreComponent
}
fun provideExpensiveObject(): ExpensiveObject
}
Core
@Component(modules = [CoreModule::class])
@Singleton
interface CoreComponent {
@[Link] interface Builder {
fun build(): CoreComponent
}
fun provideExpensiveObject(): ExpensiveObject
}
Core
@Component(modules = [CoreModule::class])
@Singleton
interface CoreComponent {
@[Link] interface Builder {
fun build(): CoreComponent
}
fun provideExpensiveObject(): ExpensiveObject
}
class OurMainApplication : Application() {
private val coreComponent: CoreComponent by lazy {
DaggerCoreComponent
.builder()
.build()
}
companion object {
@JvmStatic fun coreComponent(context: Context) =
([Link] as
OurMainApplication).coreComponent
}
}
class OurMainApplication : Application() {
private val coreComponent: CoreComponent by lazy {
DaggerCoreComponent
.builder()
.build()
}
companion object {
@JvmStatic fun coreComponent(context: Context) =
([Link] as
OurMainApplication).coreComponent
}
}
class OurMainApplication : Application() {
private val coreComponent: CoreComponent by lazy {
DaggerCoreComponent
.builder()
.build()
}
companion object {
@JvmStatic fun coreComponent(context: Context) =
([Link] as
OurMainApplication).coreComponent
}
}
Extension function
fun [Link]() =
[Link](this)
Feature 1
@Module
class Feature1Module {
@Provides
fun provideString() = "test"
}
Feature 1
@Component(modules = [Feature1Module::class],
dependencies = [CoreComponent::class])
interface Feature1Component {
@[Link]
interface Builder {
fun coreComponent(coreComponent: CoreComponent): Builder
}
fun inject(activity: OtherActivity)
}
Feature 1
@Component(modules = [Feature1Module::class],
dependencies = [CoreComponent::class])
interface Feature1Component {
@[Link]
interface Builder {
fun coreComponent(coreComponent: CoreComponent): Builder
}
fun inject(activity: OtherActivity)
}
Feature 1
val coreComponent =
[Link](context)
DaggerFeature1Component
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
Feature 2
@Module
class Feature2Module {
@Provides
fun provideInt() = 1
}
Feature 2
@Component(modules = [Feature2Module::class],
dependencies = [CoreComponent::class])
interface Feature2Component {
@[Link]
interface Builder {
fun coreComponent(coreComponent: CoreComponent): Builder
}
fun inject(activity: MainActivity)
}
When dealing with component dependencies we must follow 2 simple rules:
• An un-scoped component cannot depend on scoped components.
• A scoped component cannot depend on a component with the same scope.
@Scope
@Retention
annotation class FeatureScope
@Component(modules = [Feature1Module::class],
dependencies = [CoreComponent::class])
@FeatureScope
interface Feature1Component {
@[Link]
interface Builder {
fun coreComponent(coreComponent: CoreComponent): Builder
}
fun inject(activity: OtherActivity)
}
@Component(modules = [Feature2Module::class],
dependencies = [CoreComponent::class])
@FeatureScope
interface Feature2Component {
@[Link]
interface Builder {
fun coreComponent(coreComponent: CoreComponent): Builder
}
fun inject(activity: MainActivity)
}
Plan
interface CoreComponentProvider {
fun provideCoreComponent(): CoreComponent
}
object CoreInjectHelper {
fun provideCoreComponent(applicationContext: Context): CoreComponent{
return if (applicationContext is CoreComponentProvider) {
(applicationContext as CoreComponentProvider).provideCoreComponent()
} else {
throw IllegalStateException(
"The context passed does not implement CoreComponentProvider"
)
}
}
}
object CoreInjectHelper {
fun provideCoreComponent(applicationContext: Context): CoreComponent{
return if (applicationContext is CoreComponentProvider) {
(applicationContext as CoreComponentProvider).provideCoreComponent()
} else {
throw IllegalStateException(
"The context passed does not implement CoreComponentProvider"
)
}
}
}
object CoreInjectHelper {
fun provideCoreComponent(applicationContext: Context): CoreComponent{
return if (applicationContext is CoreComponentProvider) {
(applicationContext as CoreComponentProvider).provideCoreComponent()
} else {
throw IllegalStateException(
"The context passed does not implement CoreComponentProvider"
)
}
}
}
class OurMainApplication : Application(), CoreComponentProvider {
private lateinit var coreComponent: CoreComponent
override fun provideCoreComponent(): CoreComponent {
if (!this::[Link]) {
coreComponent = DaggerCoreComponent
.builder()
.build()
}
return coreComponent
}
}
class OurMainApplication : Application(), CoreComponentProvider {
private lateinit var coreComponent: CoreComponent
override fun provideCoreComponent(): CoreComponent {
if (!this::[Link]) {
coreComponent = DaggerCoreComponent
.builder()
.build()
}
return coreComponent
}
}
class OurMainApplication : Application(), CoreComponentProvider {
private lateinit var coreComponent: CoreComponent
override fun provideCoreComponent(): CoreComponent {
if (!this::[Link]) {
coreComponent = DaggerCoreComponent
.builder()
.build()
}
return coreComponent
}
}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var expensiveObject: ExpensiveObject
override fun onCreate(savedInstanceState: Bundle?) {
...
DaggerFeature2Component
.builder()
.coreComponent([Link]())
.build()
.inject(this)
}
}
class OtherActivity : AppCompatActivity() {
@Inject
lateinit var expensiveObject: ExpensiveObject
override fun onCreate(savedInstanceState: Bundle?) {
...
DaggerFeature1Component
.builder()
.coreComponent([Link]())
.build()
.inject(this)
}
}
[Link]
@Scope
annotation class ActivityScope
@Module(includes = [
AndroidSupportInjectionModule::class
])
abstract class AppModule {
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesMainActivityInjector(): MainActivity
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesOtherActivityInjector():
OtherActivity
}
@Module(includes = [
AndroidSupportInjectionModule::class
])
abstract class AppModule {
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesMainActivityInjector(): MainActivity
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesOtherActivityInjector():
OtherActivity
}
@Module(includes = [
AndroidSupportInjectionModule::class
])
abstract class AppModule {
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesMainActivityInjector(): MainActivity
@ActivityScope @ContributesAndroidInjector()
abstract fun contributesOtherActivityInjector():
OtherActivity
}
class OurMainApplication :
Application(),
CoreComponentProvider,
HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector:
DispatchingAndroidInjector<Activity>
private lateinit var coreComponent: CoreComponent
override fun onCreate() {
[Link]()
[Link]().inject(this)
}
override fun activityInjector(): AndroidInjector<Activity> =
dispatchingActivityInjector
}
class OurMainApplication :
Application(),
CoreComponentProvider,
HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector:
DispatchingAndroidInjector<Activity>
private lateinit var coreComponent: CoreComponent
override fun onCreate() {
[Link]()
[Link]().inject(this)
}
override fun activityInjector(): AndroidInjector<Activity> =
dispatchingActivityInjector
}
class OurMainApplication :
Application(),
CoreComponentProvider,
HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector:
DispatchingAndroidInjector<Activity>
private lateinit var coreComponent: CoreComponent
override fun onCreate() {
[Link]()
[Link]().inject(this)
}
override fun activityInjector(): AndroidInjector<Activity> =
dispatchingActivityInjector
}
class OurMainApplication :
Application(),
CoreComponentProvider,
HasActivityInjector {
@Inject lateinit var dispatchingActivityInjector:
DispatchingAndroidInjector<Activity>
private lateinit var coreComponent: CoreComponent
override fun onCreate() {
[Link]()
[Link]().inject(this)
}
override fun activityInjector(): AndroidInjector<Activity> =
dispatchingActivityInjector
}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var expensiveObject: ExpensiveObject
override fun onCreate(savedInstanceState: Bundle?) {
[Link](this)
...
}
}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var expensiveObject: ExpensiveObject
override fun onCreate(savedInstanceState: Bundle?) {
[Link](this)
...
}
}
[Link](this)
this
Activity Fragment Service Content Broadcast
Provider Receiver
Tip
@Inject
lateinit var expensiveObject: Lazy<ExpensiveObject>
[Link]()
Tip
• Always go for static provide methods.
• Nobody cares about name of your Scope
• Expose the application context through a component builder instead
of a module with constructor argument. Or even better: use the
new factory!
Tip
@Module
class Feature1Module {
@Provides
fun provideString() = "test"
}
Tip
@Module
abstract class Feature1Module {
@Binds
abstract fun provideString()
}
Tip
@Provides
Component
@Binds
Module
Factory Component
Create a inject object
Module
Create a inject object
Tip
@Binds
@IntoMap
@ViewModelKey(X1::class)
abstract bindUserViewModel(x1 : ObjX) : ObjX
Tl;Dr
• Provide dependencies through class constructors
• Avoid unnecessary scoping - Use scope annotations sparingly and
judiciously
Tl;Dr
• Use reusable components for expensive and immutable
dependencies
• Use @Binds instead of a @Provides method when simply delegating
one type to another.
• Write tests , switch dependencies on tests with a test component
instead of overriding modules.
Resources
• Using Dagger in multi-module by Marcos Holgado
• Dependency injection in a multi module project - Plaid - Ben Weiss
• Modularising Android Application by Marvin Ramin
Thats all folks!
Adit Lal
@aditlal
[Link]