question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[AssistedInject] Allow scoping of AssistedFactory with annotations

See original GitHub issue

Dagger version: 2.40 Sample project: https://github.com/ghus-raba/dagger-assisted-sample

Currently, annotating an assisted factory with scope like @Singleton takes no effect and the factory can inject also dependencies from other scopes.

In the sample project there is an AppComponent with @Singleton scope and it has a subcomponent ActivityComponent with @ActivityScope. We try to inject dependencies to ViewModel and try to ensure that only unscoped or @Singleton scoped dependencies can be injected, as its lifecycle is longer that the one of Activity.

When we do assisted injection manually, we can annotate the factory as @Singleton and it will correctly be provided by AppComponent. Trying to inject a dependency from @ActivityScope results in compilation error with [Dagger/IncompatiblyScopedBindings]:

class ManuallyAssistedViewModel constructor(
    foo: Foo,
    bar: Bar,
    assisted: Int
) : ViewModel() {

    @Singleton
    class Factory @Inject constructor(
        val fooProvider: Provider<Foo>,
        val barProvider: Provider<Bar>,
    ) {
        fun create(assisted: Int) = ManuallyAssistedViewModel(
            fooProvider.get(),
            barProvider.get(),
            assisted
        )
    }

}

However, if we do the same with @AssistedFactory, the @Singleton annotation is ignored and an Activity instance is injected.

class AssistedViewModel @AssistedInject constructor(
    foo: Foo,
    bar: Bar,
    activity: Activity,
    @Assisted assisted: Int
) : ViewModel() {

    @Singleton // this does nothing
    @AssistedFactory
    interface Factory {
        fun create(assisted: Int): AssistedViewModel
    }
}

Both of the above factories are injected in an activity like this:

private lateinit var component: ActivityComponent

@Inject internal lateinit var manuallyAssistedViewModelFactory: ManuallyAssistedViewModel.Factory
@Inject internal lateinit var assistedViewModelFactory: AssistedViewModel.Factory

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    component = application.component.activityComponentFactory.build(this)
    component.inject(this)

    // ...
}

We can explicitly provide the AssistedViewModel.Factory from AppComponent by adding a provision method val assistedFactory: AssistedViewModel.Factory, even if we do not use it and keep the injection as shown above.

It would be nice, if we could just use a scope annotation the same way as it is possible with manual way.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:11
  • Comments:15 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
ferinagycommented, Jun 9, 2022

I think maybe there is a feature request here for something more general in Dagger and implicit bindings. It sounds like if you could do @Bind Foo foo() that creates a binding inside a module and just binds it to itself to turn the implicit @Inject binding that floats to different components into an explicit binding in a module/component, that would be what you are looking for?

Yes, that sounds like a nice approach to me and I like it more than having a provision method on component that is not used at all.

It sounds like what you really want is a way to validate that the binding’s dependencies are from a particular component without affecting the scoping. For that, you should be able to use the Dagger SPI plugin.

So I had a go at the Dagger SPI plugin for validating injection of viewmodels if anyone is interested:

@AutoService(BindingGraphPlugin::class)
class ViewModelChecker : BindingGraphPlugin {

    private lateinit var types: Types
    private lateinit var viewModel: TypeElement
    private lateinit var activity: TypeElement

    override fun initElements(elements: Elements) {
        super.initElements(elements)

        viewModel = elements.getTypeElement("androidx.lifecycle.ViewModel")
        activity = elements.getTypeElement("androidx.activity.ComponentActivity")
    }

    override fun initTypes(types: Types) {
        super.initTypes(types)

        this.types = types
    }

    override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) {
        val activityBindings = mutableListOf<Binding>()
        val viewModelBindings = mutableListOf<Binding>()

        bindingGraph.bindings().forEach {
            if (it.kind() == BindingKind.BOUND_INSTANCE && types.isSubtype(it.key().type(), activity.asType())) {
                activityBindings += it
            }

            if (types.isSubtype(it.key().type(), viewModel.asType())) {
                viewModelBindings += it
            }
        }

        val activityComponents = activityBindings.map { it.componentPath().currentComponent() }.toSet()

        viewModelBindings.forEach { viewModelBinding ->
            viewModelBinding.scope().ifPresent {
                diagnosticReporter.reportBinding(
                    Diagnostic.Kind.ERROR,
                    viewModelBinding,
                    "ViewModel %s has a scope %s. ViewModel's lifecycle is managed via ViewModelStoreOwner, so it should not be scoped via dagger.",
                    viewModelBinding.key(),
                    it
                )
            }

            if (activityComponents.any { it in viewModelBinding.componentPath() }) {
                diagnosticReporter.reportBinding(
                    Diagnostic.Kind.WARNING,
                    viewModelBinding,
                    "ViewModel %s is included in activity component",
                    viewModelBinding.key()
                )
            }
        }
    }
}

private operator fun ComponentPath.contains(other: TypeElement): Boolean = when {
    currentComponent() == other -> true
    atRoot() -> false
    else -> other in parent()
}
0reactions
Chang-Ericcommented, Jun 6, 2022

Thanks for looking into it, and yea, that is kind of weird, but probably not worth the effort to debug given it is so long ago and it would have been a bug anyway for that to require a scope annotation.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Assisted Injection - Dagger
The factory must be annotated with @AssistedFactory and must contain an abstract method that returns the @AssistedInject type and takes in all @Assisted ......
Read more >
Brave New Android World with AssistedInject - ProAndroidDev
Assisted Inject is interesting feature of dependency injection, but looks very weird at a first grasp. Let me explain when it could be...
Read more >
Assisted Inject for less boilerplate? - FunkyMuse
The dependency itself, annotated with @AssistedInject constructor(); The factory (interface), annoated with @AssistedFactory ...
Read more >
Can I use some kind of assisted Inject with Dagger?
@AssistedFactory interface SomeEditorFactory { SomeEditor ... compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.4.0' ...
Read more >
Assisted Injection With Dagger and Hilt - RayWenderlich.com
This allows you to update the code in MainActivity.kt to the ... Define a Factory implementation with the @AssistedFactory annotation.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found