Components and Scoping in Hilt
August 2, 2020
Updated April 4, 2021
Components
Both Dagger and Hilt have a concept of Component which is a dependencies container that follows the Android lifecycle. But unlike Dagger, Hilt users never define or instantiate Dagger components directly. Instead, Hilt offers predefined components that are generated for you. Because of that it’s easier to grok.
Here are the predefined Components in Hilt and their hierarchy:
SingletonComponent
ActivityRetainedComponent
ViewModelComponent
ActivityComponent
FragmentComponent
ViewComponent
ViewWithFragmentComponent
ServiceComponent
Let’s look at each of them in detail.
SingletonComponent
SingletonComponent
is a top-most component in Hilt component hierarchy. It will exist as long as the app is alive. This is a good place to define application-wide bindings such as Repositories, API, SharedPreferences, etc. This is the most long-lived Component in your app.
ActivityRetainedComponent
ActivityRetainedComponent
will exist for the duration of Activity lifetime even if the Activity is destroyed and recreated due to configuration change.
ViewModelComponent
ViewModelComponent
will follow ViewModel
lifecycle which is shorter than Application lifecycle but longer than Activity
and Fragment lifecycles because ViewModels survive orientation and configuration changes. Both SingletonComponent
and ActivityRetainedComponent
are parents of this Component—so you’ll be able to depend on all bindings defined in SingletonComponent
and ActivityRetainedComponent
. Note: ViewModelComponent
also contains a default binding of the SavedStateHandle
associated with its ViewModel.
ActivityComponent
ActivityComponent
follows Activity
lifecycle. When an Activity that this Component is attached to is destroyed, the bindings in this Component will be destroyed as well. ActivityRetainedComponent
is a parent of this component so you’ll be able to depend on all bindings defined in both ActivityRetainedComponent
and SingletonComponent.
FragmentComponent
FragmentComponent
follows Fragment lifecycle. ActivityComponent
is a parent of this Component so you’ll be able to depend on all bindings defined in the ActivityComponent
, ActivityRetainedComponent
and SingletonComponent
.
ViewComponent
ViewComponent
follows View attached to Activity lifecycle. ActivityComponent
is a parent of this Component so you’ll be able to depend on all bindings defined in the ActivityComponent
, ActivityRetainedComponent
and SingletonComponent.
ViewWithFragmentComponent
ViewWithFragmentComponent
requires and instance of the View and the Fragment that this View is attached to. During its lifecycle, Fragment instance will go through onAttach and then onCreate and exist until onDestroy followed by onDetach. If this Fragment has views, they will be inflated in onCreateView and destroyed in onDestroyView (callbacks that occur between onCreate and onDestroy of the Fragment instance). During that time, ViewWithFragmentComponent
will exist. If the user then navigates deeper in the app causing the Fragment to be added to backstack while staying in memory, each view will be destroyed and its references to the Fragment will be cleaned up in onDestroyView
. At that point the Fragment still exists but the views don’t, so the ViewWithFragmentComponent
will be destroyed as well. The FragmentComponent
still exists at that point. When the user navigates back to the Fragment instance, its views will be re-inflated again and ViewWithFragmentComponent
will become active again.
You must use the @WithFragmentBindings
annotation with @AndroidEntryPoint
for these views.
To sum up, the FragmentComponent
is a parent of the ViewWithFragmentComponent
and will exist even when Fragment’s View no longer exists. ViewWithFragmentComponent
has access to all bindings defined in the FragmentComponent
, ActivityComponent
, ActivityRetainedComponent
and SingletonComponent
.
ServiceComponent
ServiceComponent
follows Service lifecycle. SingletonComponent
is a parent of this Component. So you will get access to all bindings defined in SingletonComponent
.
Binding to Components
In order to install dependencies into a Component, you need to use @InstallIn
annotation.
@InstallIn can only be used on @Module or @EntryPoint classes.
@Module
@InstallIn(SingletonComponent::class) @Module object NetworkModule { }
@EntryPoint
class MyContentProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface MyContentProviderEntryPoint { fun analyticsHelper(): AnalyticsHelper } ... }
And then when our MyContentProvider
needs the AnalyticsHelper binding, it can get by using:
val entryPoint = EntryPointAccessors.fromApplication(appContext, MyContentProviderEntryPoint::class.java) val analyticsHelper = entryPoint.analyticsHelper()
Entry Points (interfaces annotate with @EntryPoint
) provide access to the Dagger object graph in a given component for code that otherwise does not have access to the object graph. For instance, the ContentProvider
above cannot be added to our object graph since it can exist before Application.onCreate()
is executed. If the ContentProvider needs a binding from our object graph, the binding gets exposed from a given component as an interface by using an @EntryPoint
annotation.
Component Internals
@DefineComponent
annotation is used internally to define components:
import dagger.hilt.DefineComponent import javax.inject.Singleton @Singleton @DefineComponent interface SingletonComponent
And here is a predefined FragmentComponent
whose parent is the ActivityComponent
.
import dagger.hilt.DefineComponent import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.scopes.FragmentScoped @FragmentScoped @DefineComponent(parent = ActivityComponent::class) interface FragmentComponent
You can define custom Components but there are some limitations at the moment.
Components must be a direct or indirect child of the
SingletonComponent
Custom Components may not be inserted between any of the standard components. For example, a component cannot be added between the
ActivityComponent
and theFragmentComponent
Keep in mind that each component/scope adds cognitive overhead and therefore should be used sparingly. Custom components work against standardization—the more custom Components are used, the harder it is to work with the object graph and to use them in shared libraries.
Scoping
Each Component referenced via @InstallIn has a corresponding scope annotation:
All dependency bindings defined in a Module or via annotating classes with @Inject constructors are unscoped by default. Every time you ask for an unscoped dependency within your component, new instance of that dependency will be generated and returned to you.
For instance in this code the AvatarFetcher
is unscoped:
@InstallIn(ActivityComponent::class) @Module object UserProfileModule { @Provides fun provideAvatarFetcher(): AvatarFetcher { } }
Every time an Activity in your app asks for an AvatarFetcher
, a new instance of AvatarFetcher
will be returned even though the dependency binding is installed in a Container.
If you don’t want to create new
AvatarFetcher
every time you need to inject one, you need to scope it so the same instance is returned every time.
Each Component maps to a preconfigured scope in Hilt. For example, a binding within an @InstallIn(ActivityComponent.class)
module can only be scoped with @ActivityScoped
.
@InstallIn(ActivityComponent::class) @Module object UserProfileModule { @ActivityScoped @Provides fun provideAvatarFetcher(): AvatarFetcher { ... } }
Or if you have control over AvatarFetcher and use @Inject constructor:
@ActivityScoped class AvatarFetcher @Inject constructor() { ... }
Here is a mapping of the built-in Components and their preconfigured scopes, see https://developer.android.com/training/dependency-injection/hilt-android#component-hierarchy
Should you always scope your dependencies? Do it only when it’s necessary for the app to work correctly. For instance, your class instance may maintain some state which needs to be factored in every time you ask for that object. Scoping adds overhead in both the generated code size and runtime performance. Often creating lightweight objects every time is better than managing and returning the same object.
Warning: A common misconception is that all fragment instances will share the same instance of a binding scoped with
@FragmentScoped
. However, this is not true. Each fragment instance gets a new instance of the fragment component, and thus a new instance of all its scoped bindings.
So if your AvatarFetcher is scoped to a FragmentComponent
@FragmentScoped class AvatarFetcher @Inject constructor() { ... }
And you have 2 Fragments asking AvatarFetcher
.
Here is a first Fragment:
@AndroidEntryPoint class RegistrationFragment : Fragment() { @Inject lateinit var avatarFetcher: AvatarFetcher ... }
And another one:
@AndroidEntryPoint class ProfileFragment : Fragment() { @Inject lateinit var avatarFetcher: AvatarFetcher ... }
Every time you ask for an instance of the AvatarFetcher
in RegistrationFragment
, it’s going to be the same instance of AvatarFetcher.
Every time you ask for an instance of the AvatarFetcher
in ProfileFragment
, it’s going to be the same instance of AvatarFetcher
but this instance will be different from the AvatarFetcher
instance that the RegistrationFragment
gets.
To re-iterate, each Fragment gets its own FragmentComponent container and bindings within it are available to that Fragment only. Scoped bindings guarantee one instance per that instance of the Container.
ViewModel scoping
ViewModels can be scoped to a Fragment, a host Activity or a navigation graph.
First you need to inject a ViewModel using @ViewModelInject
available in hilt-lifecycle-viewmodel Gradle dependency:
class ExampleViewModel @ViewModelInject constructor() : ViewModel() { ... }
Then, if your Fragment is using a ViewModel that should be scoped to that Fragment, use by viewModels()
delegate:
@AndroidEntryPoint class RegistrationFragment : Fragment() { private val viewModel: RegistrationViewModel by viewModels() }
If your Fragment is using a ViewModel should be scoped to a host Activity, use by activityViewModels()
delegate:
@AndroidEntryPoint class RegistrationFragment : Fragment() { private val viewModel: RegistrationViewModel by activityViewModels() }
Using a ViewModel from an Activity requires using by viewModels()
delegate:
@AndroidEntryPoint class RegistrationActivity : AppCompatActivity() { private val viewModel: RegistrationViewModel by viewModels() }
If you are using Jetpack Navigation library, you can have custom scopes (for instance, a scope for Onboarding flow, Checkout flow, etc). In that case, if your ViewModel
is scoped to the navigation graph, use the defaultViewModelProviderFactory
object that is available to Activities and Fragments that are annotated with @AndroidEntryPoint:
val viewModel: ExampleViewModel by navGraphViewModels(R.id.my_graph) { defaultViewModelProviderFactory }