Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Effective DI with Multi-Modular Architecture

Adit Lal
December 15, 2020

Effective DI with Multi-Modular Architecture

Dividing the module of a Monolithic Android app to realise multi-module application is considered as an important concern in recent Android development.

This session distils all the pitfalls and best practices in building a complex app and take a look at the usefulness of multi-module application and how we implemented Dependency Injection in our consumer app at Gojek(superapp.is)

Some of the highlight points of this session are :

1. Reintroduction to DI
2. Reintroduction to Dagger-Android
3. DI in single module projects
4. DI hotness in multi-module projects
5. Realise DI in multi-module projects using Dagger2

Adit Lal

December 15, 2020
Tweet

More Decks by Adit Lal

Other Decks in Technology

Transcript

  1. Adit Lal GoPay $6.5B @aditlal We have 20+ products from

    food tech to fin-tech to hyper local delivery and massage services Ride Sharing 20m KM
  2. • Organise and decouple independent pieces of functionality • Improve

    project build times • Define clear ownership boundaries between different teams Thoughts on modularisation
  3. 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
  4. App module : • No feature-specific code • No infrastructure-specific

    code. • Creates the dagger component Thoughts on modularisation
  5. Ownership? core auth common Feature n-1 Feature n+2 Feature n-2

    Feature z Feature n Feature n+1 app … … … … …
  6. Ownership? core auth common Feature n-1 Feature n+2 Feature n-2

    Feature z Feature n Feature n+1 app … … … … …
  7. • 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. IoC Principle
  8. • We should prefer to be explicit with the module

    dependencies • We don’t want CoreComponent to know about FeatureComponent • It can’t check if the graph is correct. SubComponent goes away! It needs the parent = slower compilation
  9. @Component(modules = [CoreModule::class]) @Singleton interface CoreComponent { @Component.Builder interface Builder

    { fun build(): CoreComponent } fun provideExpensiveObject(): ExpensiveObject } Core
  10. class OurMainApplication : Application() { private val coreComponent: CoreComponent by

    lazy { DaggerCoreComponent .builder() .build() } companion object { @JvmStatic fun coreComponent(context: Context) = (context.applicationContext as OurMainApplication).coreComponent } }
  11. class OurMainApplication : Application() { private val coreComponent: CoreComponent by

    lazy { DaggerCoreComponent .builder() .build() } companion object { @JvmStatic fun coreComponent(context: Context) = (context.applicationContext as OurMainApplication).coreComponent } }
  12. class OurMainApplication : Application() { private val coreComponent: CoreComponent by

    lazy { DaggerCoreComponent .builder() .build() } companion object { @JvmStatic fun coreComponent(context: Context) = (context.applicationContext as OurMainApplication).coreComponent } }
  13. @Component(modules = [Feature1Module::class], dependencies = [CoreComponent::class]) interface Feature1Component { @Component.Builder

    interface Builder { fun coreComponent(coreComponent: CoreComponent): Builder } fun inject(activity: OtherActivity) } Feature 1
  14. @Component(modules = [Feature2Module::class], dependencies = [CoreComponent::class]) interface Feature2Component { @Component.Builder

    interface Builder { fun coreComponent(coreComponent: CoreComponent): Builder } fun inject(activity: MainActivity) } Feature 2
  15. 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.
  16. @Component(modules = [Feature1Module::class], dependencies = [CoreComponent::class]) @FeatureScope interface Feature1Component {

    @Component.Builder interface Builder { fun coreComponent(coreComponent: CoreComponent): Builder } fun inject(activity: OtherActivity) }
  17. @Component(modules = [Feature2Module::class], dependencies = [CoreComponent::class]) @FeatureScope interface Feature2Component {

    @Component.Builder interface Builder { fun coreComponent(coreComponent: CoreComponent): Builder } fun inject(activity: MainActivity) }
  18. Component.dependencies • We can choose what we expose from each

    component • more verbosity • we can get dependencies from multiple components faster compilation
  19. 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" ) } } }
  20. 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" ) } } }
  21. 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" ) } } }
  22. class OurMainApplication : Application(), CoreComponentProvider { private lateinit var coreComponent:

    CoreComponent override fun provideCoreComponent(): CoreComponent { if (!this::coreComponent.isInitialized) { coreComponent = DaggerCoreComponent .builder() .build() } return coreComponent } }
  23. class OurMainApplication : Application(), CoreComponentProvider { private lateinit var coreComponent:

    CoreComponent override fun provideCoreComponent(): CoreComponent { if (!this::coreComponent.isInitialized) { coreComponent = DaggerCoreComponent .builder() .build() } return coreComponent } }
  24. class OurMainApplication : Application(), CoreComponentProvider { private lateinit var coreComponent:

    CoreComponent override fun provideCoreComponent(): CoreComponent { if (!this::coreComponent.isInitialized) { coreComponent = DaggerCoreComponent .builder() .build() } return coreComponent } }
  25. class MainActivity : AppCompatActivity() { @Inject lateinit var expensiveObject: ExpensiveObject

    override fun onCreate(savedInstanceState: Bundle?) { ... DaggerFeature2Component .builder() .coreComponent(this.coreComponent()) .build() .inject(this) } }
  26. class MainActivity : AppCompatActivity() { @Inject lateinit var expensiveObject: ExpensiveObject

    override fun onCreate(savedInstanceState: Bundle?) { ... DaggerFeature2Component .builder() .coreComponent(this.coreComponent()) .build() .inject(this) } }
  27. @Module(includes = [ AndroidSupportInjectionModule::class ]) abstract class AppModule { @ActivityScope

    @ContributesAndroidInjector() abstract fun contributesMainActivityInjector(): MainActivity @ActivityScope @ContributesAndroidInjector() abstract fun contributesOtherActivityInjector(): OtherActivity }
  28. @Module(includes = [ AndroidSupportInjectionModule::class ]) abstract class AppModule { @ActivityScope

    @ContributesAndroidInjector() abstract fun contributesMainActivityInjector(): MainActivity @ActivityScope @ContributesAndroidInjector() abstract fun contributesOtherActivityInjector(): OtherActivity }
  29. class OurMainApplication : Application(), CoreComponentProvider, HasActivityInjector { @Inject lateinit var

    dispatchingActivityInjector: DispatchingAndroidInjector<Activity> private lateinit var coreComponent: CoreComponent override fun onCreate() { super.onCreate() DaggerAppComponent.create().inject(this) } override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector }
  30. class OurMainApplication : Application(), CoreComponentProvider, HasActivityInjector { @Inject lateinit var

    dispatchingActivityInjector: DispatchingAndroidInjector<Activity> private lateinit var coreComponent: CoreComponent override fun onCreate() { super.onCreate() DaggerAppComponent.create().inject(this) } override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector }
  31. class OurMainApplication : Application(), CoreComponentProvider, HasActivityInjector { @Inject lateinit var

    dispatchingActivityInjector: DispatchingAndroidInjector<Activity> private lateinit var coreComponent: CoreComponent override fun onCreate() { super.onCreate() DaggerAppComponent.create().inject(this) } override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector }
  32. class OurMainApplication : Application(), CoreComponentProvider, HasActivityInjector { @Inject lateinit var

    dispatchingActivityInjector: DispatchingAndroidInjector<Activity> private lateinit var coreComponent: CoreComponent override fun onCreate() { super.onCreate() DaggerAppComponent.create().inject(this) } override fun activityInjector(): AndroidInjector<Activity> = dispatchingActivityInjector }
  33. class MainActivity : AppCompatActivity() { @Inject lateinit var expensiveObject: ExpensiveObject

    override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) ... } }
  34. class MainActivity : AppCompatActivity() { @Inject lateinit var expensiveObject: ExpensiveObject

    override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) ... } }
  35. • 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
  36. • Provide dependencies through class constructors • Avoid unnecessary scoping

    - Use scope annotations sparingly and judiciously Tl;Dr
  37. • 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. Tl;Dr
  38. • Using Dagger in multi-module by Marcos Holgado • Dependency

    injection in a multi module project - Plaid - Ben Weiss • Modularising Android Application by Marvin Ramin Resources