$30 off During Our Annual Pro Sale. View Details »

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. Effective DI with
    Multi-Modular Architecture

    View Slide

  2. 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

    View Slide

  3. app
    core
    feature 2
    feature 1
    Monolith
    Expensive
    Object

    View Slide

  4. What could possibly go wrong?

    View Slide

  5. Solve DI with
    Multi-Modular codebase

    View Slide

  6. Modularise?

    View Slide

  7. app
    core
    Feature B
    Feature A
    Feature modules
    Expensive
    Object

    View Slide

  8. • Organise and decouple independent pieces of functionality
    • Improve project build times
    • Define clear ownership boundaries between different teams
    Thoughts on modularisation

    View Slide

  9. 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

    View Slide

  10. App module :
    • No feature-specific code
    • No infrastructure-specific code.
    • Creates the dagger component
    Thoughts on modularisation

    View Slide

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

    … … …

    View Slide

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

    … … …

    View Slide

  13. Inversion
    of control
    Dependency
    Inversion
    Dependency
    Injection
    IoC Principle

    View Slide

  14. • 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

    View Slide

  15. Should I do Modularise to use Dagger?

    View Slide

  16. Don’t do it for Dagger , do it for yourself

    View Slide

  17. :feature1
    F1SubComponent
    F1Module
    :feature2
    F2SubComponent
    F2Module
    :core
    CoreComponent
    CoreModule
    Expensive
    Object
    :app
    MainComponent
    MainModule

    View Slide

  18. :app
    MainComponent
    MainModule
    :feature1
    F1Component
    F1Module
    :feature2
    F2Component
    F2Module
    :core
    CoreComponent
    CoreModule
    Expensive
    Object

    View Slide

  19. • 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

    View Slide

  20. Core
    @Module
    class CoreModule {
    @Provides
    @Singleton
    fun provideExpObj(): ExpensiveObject = ExpensiveObject()
    }

    View Slide

  21. @Component(modules = [CoreModule::class])
    @Singleton
    interface CoreComponent {
    @Component.Builder interface Builder {
    fun build(): CoreComponent
    }
    fun provideExpensiveObject(): ExpensiveObject
    }
    Core

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  25. fun Activity.coreComponent() =
    OurMainApplication.coreComponent(this)
    Extension function

    View Slide

  26. @Module
    class Feature1Module {
    @Provides
    fun provideString() = "test"
    }
    Feature 1

    View Slide

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

    View Slide

  28. DaggerFeature1Component
    .builder()
    .coreComponent(this.coreComponent)
    .build()
    .inject(this)
    Feature 1

    View Slide

  29. @Module
    class Feature2Module {
    @Provides
    fun provideInt() = 1
    }
    Feature 2

    View Slide

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

    View Slide

  31. 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.

    View Slide

  32. @Scope
    @Retention
    annotation class FeatureScope

    View Slide

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

    View Slide

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

    View Slide

  35. Component.dependencies
    • We can choose what we expose from each component
    • more verbosity
    • we can get dependencies from multiple components
    faster compilation

    View Slide

  36. Let’s get to it

    View Slide

  37. interface CoreComponentProvider {
    fun provideCoreComponent(): CoreComponent
    }

    View Slide

  38. 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"
    )
    }
    }
    }

    View Slide

  39. 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"
    )
    }
    }
    }

    View Slide

  40. 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"
    )
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  46. Dagger.Android

    View Slide

  47. @Scope
    annotation class ActivityScope

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. AndroidInjection.inject(this)
    Activity Fragment Service Content
    Provider
    Broadcast
    Receiver
    this

    View Slide

  57. Tip
    @Inject
    lateinit var expensiveObject: Lazy
    expensiveObject.get()

    View Slide

  58. • 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

    View Slide

  59. @Module
    class Feature1Module {
    @Provides
    fun provideString() = "test"
    }
    Tip

    View Slide

  60. @Module
    abstract class Feature1Module {
    @Binds
    abstract fun provideString()
    }
    Tip

    View Slide

  61. Tip
    Module
    Module
    Factory
    Component
    @Provides
    Create a inject object
    Component
    @Binds
    Create a inject object

    View Slide

  62. Tip
    @Binds
    @IntoMap
    @ViewModelKey(X1::class)
    abstract bindUserViewModel(x1 : ObjX) : ObjX

    View Slide

  63. • Provide dependencies through class constructors
    • Avoid unnecessary scoping - Use scope annotations sparingly and
    judiciously
    Tl;Dr

    View Slide

  64. • 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

    View Slide

  65. • 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

    View Slide

  66. https://superapp.is
    https://www.gojek.io

    View Slide

  67. Adit Lal
    @aditlal
    aditlal.dev
    Thats all folks!
    http://bit.ly/3a6Bid4

    View Slide