Dynamic feature and regular modules using Dagger2

When your app contains a dynamic feature module (downloaded and installed on demand) and regular feature modules, setting up the DI graph…

Dynamic feature and regular modules using Dagger2

When your app contains a dynamic feature module (downloaded and installed on demand) and regular feature modules, setting up the DI graph poses some limitations.

The dependency of dynamic features is reversed. The module knows about App, but App ‘hardly’ knows the dynamic feature and cannot access its classes. In the App’s build file this distinction shows as:

android { ...
    dynamicFeatures = [":my_dynamic_feature"]
}

dependencies { ...
    implementation project(':core')
}

As you can see in the graph, Core and Regular Feature module do not reference the App module. This enforces separation of concerns. However, this is not possible for the dynamic feature module: it must hard-reference App as a dependency.

The problem

The main problem is that the DI root in App cannot access Dynamic Feature. Or in other words: the dagger-android tree cannot be constructed because it hard-codes the Subcomponents from the ContributesAndroidInjector annotated classes. It generates this code at compile time and to do so, it needs references to the components and modules it is connecting.

Towards the solution

One could try to inject components using dagger-android and this solution, but it involves reflection. Dagger’s Subcomponents are always declared top-down and are therefore unsuited. Dagger’s regular Components are connected more loosely and ‘sideways’.

The Plaid app uses this setup using Components. In a diagram it looks like this:

Plaid uses only dynamic features. When mixing the solution with regular feature modules, the solution will not work well because all modules need to know App in this setup. It would result in circular dependencies:

The solution

After setting up the dependent Components approach, we move the DI to the Core module, then the app dependencies will become clean:

The root of the DI graph is not constructed in App, but in Core, using dependent Components to make sure Core does not need to know the other Components in the graph.

Implementation details

The Core module contains the DI’s core Component:

@Component(modules = [...])
interface CoreComponent {

The other components, of the feature modules, all reference this CoreComponent:

@Component(
    dependencies = [CoreComponent::class],
    modules = [...]
)
interface DynamicFeatureComponent {

The Core module has an interface class that any Application using it should implement:

interface CoreComponentProvider {
    fun provideCoreComponent(): CoreComponent
}

The App module constructs the CoreComponent’s object:

class MainApplication : Application(), CoreComponentProvider {

    lateinit var coreComponent: CoreComponent

    override fun onCreate() {
        super.onCreate()

        coreComponent = DaggerCoreComponent
            .builder()
            .contextModule(AppContextModule(this))
            .build()
    }

    override fun provideCoreComponent() = coreComponent
}

Each module uses this CoreComponent and adds its own Components. For example in a feature module’s Fragment:

override fun onAttach(context: Context?) {
    super.onAttach(context)

    DynamicFeatureComponent
        .builder()
        .coreComponent(requireActivity().coreComponent())
        .dynamicFeatureModule(DynamicFeatureModule(this))
        .build()
        .inject(this)
}

DynamicFeatureModule is just a regular module, AppContextModule provides the context:

@Module
class AppContextModule(val application: Application) {

    @Singleton
    @Provides
    fun provideContext(): Context = application
}

And coreComponent() is an extension function:

fun Activity.coreComponent() = (applicationContext as? CoreComponentProvider)?.provideCoreComponent()
        ?: throw IllegalStateException("CoreComponentProvider not implemented: $applicationContext")

Note that linked components should have distinct scopes.

Also note that you need to define the classes that CoreComponent provides explicitly in provision methods, otherwise they will not be shared with linked components:

...
interface CoreComponent {

    fun context(): Context
    fun firebaseFirestore(): FirebaseFirestore
    ...

That’s all. An example project:

Frank1234/DynamicDaggerTest
Contribute to Frank1234/DynamicDaggerTest development by creating an account on GitHub.github.com

After writing this article I noticed that handling Dynamic Feature Modules is very close to Instant App handling. A lot has been written on this subject. An interesting read would be:

Dagger2 for Modular Architecture
When designing a modular architecture or an Instant App the vertical dependencies can have only one direction: Feature…medium.com


Check out our Engineering Blog for more in depth stories about pragmatic code for happy users!