Search code examples
androidkotlindependency-injectiondagger-2dagger

Dagger 2 Inject Dependency from custom scope inside my viewmodel


I am trying to create a custom scope for some of my objects, to use them in viewmodels and other places withing my app that do not belong in this scope.

This is what my component and subcomponent look like

@Singleton
@Component(modules = [
    AndroidSupportInjectionModule::class,
    AppModule::class,
    ViewModelModule::class,
    AppSubComponents::class
])
internal interface AppComponent : AndroidInjector<MyApplication> {
    @Component.Factory
    abstract class Factory : AndroidInjector.Factory<MyApplication>
}

@Module(subcomponents = [SessionComponent::class])
abstract class AppSubComponents

@SessionScope
@Subcomponent(modules = [SessionModule::class])
interface SessionComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): SessionComponent
    }
}

My ViewModelModule:

@Module
abstract class ViewModelModule {

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(MyViewModelImpl::class)
    internal abstract fun bindMyViewModelImpl(viewModel: MyViewModelImpl): ViewModel
}

and my SessionModule:

@Module
class SessionModule {


    @Provides
    @SessionScope
    fun providesTestInjectClass() : TestInjectClass = TestInjectClass()

}

The viewmodel class is injected by the constructor, depends on TestInjectClass and is not annotated with a scope

class MyViewModelImpl @Inject constructor(
        private val testInject: TestInjectClass
) : ViewModel() {

    ...
}

Now inside my Fragment as expected I am instantiating my viewmodel like viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[MyViewModelImpl::class.java]

but I end up with the following error

error: [Dagger/MissingBinding] com.example.TestInjectClass cannot be provided without an @Inject constructor or an @Provides-annotated method.

And it gives me the dependency path of how it reached needing this dependency.

From what I have understood so far, because my viewmodel is not scoped and belongs to the main component, it cannot find injection logic for my TestInjectClass that is scoped with SessionScope

Is there a way to fix this without making the viewmodel scoped to SessionScope ?


Solution

  • Bindings within SessionComponent are not automatically available across your AppComponent. This is in part to enable better encapsulation and in part because you're presumably scoping in order to reuse the same @SessionScope object instances within the same session and Dagger has no idea how to find the relevant session (i.e. SessionComponent instance) when you ask for an object bound in SessionComponent.

    For what it's worth, SessionComponent can use anything bound in its parent components (AppComponent here) whether or not they have scoping (@Singleton here). Furthermore, because you've installed the subcomponent in a module, you can inject SessionComponent.Factory from anywhere within AppComponent.

    You'll have to manage your SessionComponent access yourself: Through your SessionComponent.Factory you can create those component instances wherever you need them and keep them as long as they are relevant. Another way to do this is to create your own session manager class that creates or retrieves SessionComponent instances and cleans them up when necessary.

    To make TestInjectClass available, you'll need to expose it through a getter on SessionComponent or other similar means (like members injection or by providing a container object that contains your TestInjectClass).

    @SessionScope
    @Subcomponent(modules = [SessionModule::class])
    interface SessionComponent {
        fun testInjectClass(): TestInjectClass
    
        // [factory here]
    }
    

    If your Android object graph tracks well to sessions, you can also bind a SessionComponent binding within your Activity or Fragment module/subcomponent so you can directly inject a SessionComponent from those places. You could even make TestInjectClass available directly by finding/fetching a relevant SessionComponent and calling testInjectClass() on it. The simplest of those would look like this:

    fun provideTestInjectClass(factory: SessionComponent.Factory) =
        factory.create().testInjectClass()
    

    However, remember that that's going to create a new SessionComponent every time you ask Dagger for a TestInjectClass, so it's probably only the right call if TestInjectClass is the main class in your SessionComponent and you don't mind passing it around carefully.