Search code examples
androiddependency-injectiondagger-2dagger-hilt

Does Using ActivityComponent Scope in Hilt Provide the Same Dependency Instance Across All Fragments in the Activity


As we can see in the attached picture from the Google website, The SingletonComponent will provide the same dependency instance across the application. Is this the same for ActivityComponent? Will it provide the same dependency instance for all the fragments in that activity? I tried this code below, and I got two different IDs of the navigator instance!

// AppNavigator.kt

interface AppNavigator {
    fun navigate()
}

// AppNavigatorImpl.kt

class AppNavigatorImpl @Inject constructor() : AppNavigator {
    val id = UUID.randomUUID().toString()
    override fun navigate() {
        Log.d("AppNavigator", "Navigating with instance $id")
        //navigation logic
    }
}

// NavigationModule.kt

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}

// MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var navigator: AppNavigator

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        (navigator as AppNavigatorImpl).navigate()
    }
}

// Fragment1.kt

@AndroidEntryPoint
class Fragment1 : Fragment() {
    @Inject lateinit var navigator: AppNavigator

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        (navigator as AppNavigatorImpl).navigate()
    }
}

enter image description here


Solution

  • For a given Activity instance, bindings installed in ActivityComponent will return the same instance as long as they are annotated with @ActivityScoped. This includes bindings that are installed in ActivityComponent but that are injected through one of its transitive subcomponents like FragmentComponent. You'll probably want to annotate AppNavigatorImpl with a class-level @ActivityScoped annotation to get the behavior you want.

    // In NavigationModule, installed in ActivityComponent, but not @ActivityScoped
    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
    

    Scoped bindings in Dagger work by storing the instance for each scoped binding within the Component with the same scope. Here, Hilt is generating an ActivityComponent annotated with @ActivityScoped, so you can install @ActivityScoped bindings into ActivityComponent (as you have via @InstallIn); it will store instances there and always return the same one. However, bindings in Dagger are not scoped by default, and you can install unscoped bindings into any component in Dagger: for those, Dagger will return a new instance every time you ask for one. (@Reusable is likewise special-cased so reusable bindings can be installed in any component.)

    Here, neither your Navigator instance nor its implementation have a scope annotation, so every time you ask for an AppNavigator, Dagger uses the @Binds binding in the Module to refer to AppNavigatorImpl, and then uses the @Inject constructor in the AppNavigatorImpl to generate a new instance.

    Technically you have a choice about whether to annotate AppNavigator (by annotating the @Binds method) or AppNavigatorImpl (by annotating the class). However, if you annotate AppNavigator, then every AppNavigator injection will inject the same AppNavigatorImpl instance but any explicit AppNavigatorImpl injections will inject a different AppNavigatorImpl instance, and that could get confusing. If you only expect one AppNavigatorImpl instance to be available on your graph, annotate the impl itself with @ActivityScoped and you'll be set to go.

    @ActivityScoped class AppNavigatorImpl @Inject constructor() : AppNavigator {
        // ...
    }
    

    For what it's worth, all of this applies to the other scopes in the posted picture, including @Singleton and @FragmentScoped. To get the instance-conserving behavior you want, you'll have to install into the right component and also annotate with the right annotation. Scoped bindings are somewhat expensive—the component instantiates extra holders and @Singleton objects (for instance) can never be garbage collected through the lifetime of your app—so be conservative about how many bindings you annotate.