Migrating From Dagger to Hilt
Learn about Hilt and its API. Discover how Hilt facilitates working with Dagger by migrating the code of an existing app from Dagger to Hilt. By Massimo Carli.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Migrating From Dagger to Hilt
20 mins
- Getting Started
- Understanding Hilt Design Principles
- Using Predefined Scopes and Components
- Adding Hilt Dependencies
- Fixing ApplicationComponent
- Understanding Hilt’s Code Generation
- Removing AppComponent
- Refactoring @FeatureScope
- Understanding Entry Points
- Using @AndroidEntryPoint With Activities
- Using @AndroidEntryPoint With Fragments
- What About @Scopes?
- IDE Support for Hilt
- Where to Go From Here?
Understanding Hilt’s Code Generation
Select the Project View in Android Studio and open app/build/generated/source/kapt/debug. Next, look at what’s inside the generated code for the init package. You’ll see this:
This is all generated code that could change in future versions of Hilt.
Open InitApp_HiltComponents.java to find the definitions of all the predefined components and scopes.
If you focus on ActivityComponent
, for example, you’ll see something like the following code:
@Generated("dagger.hilt.processor.internal.root.RootProcessor")
public final class InitApp_HiltComponents {
private InitApp_HiltComponents() {
}
@Module(
subcomponents = ActivityC.class
)
@DisableInstallInCheck
@Generated("dagger.hilt.processor.internal.root.RootProcessor")
abstract interface ActivityCBuilderModule {
@Binds
ActivityComponentBuilder bind(ActivityC.Builder builder);
}
- - -
@Subcomponent(
modules = {
DefaultViewModelFactories.ActivityModule.class,
HiltWrapper_ActivityModule.class,
FragmentCBuilderModule.class,
ViewCBuilderModule.class
}
)
@ActivityScoped
public abstract static class ActivityC implements ActivityComponent,
DefaultViewModelFactories.ActivityEntryPoint,
FragmentComponentManager.FragmentComponentBuilderEntryPoint,
ViewComponentManager.ViewComponentBuilderEntryPoint,
GeneratedComponent {
@Subcomponent.Builder
abstract interface Builder extends ActivityComponentBuilder {
}
}
- - -
}
This is the first step toward automatically generating all the code for the components you need in the app.
If you look at this code carefully, you’ll notice it contains Dagger annotations like @Module
, @Subcomponent
and more. You also see custom predefined @Scope
s like @ActivityScoped
.
This is the code you’d need to write on your own if you wanted to create the same @Component
or @Subcomponent
hierarchy that Hilt gives you automatically. This is also proof that Hilt isn’t a different entity from Dagger — it’s simply a tool that makes Dagger easier.
Removing AppComponent
You already learned that Hilt provides you with ApplicationComponent
and that you don’t need to create a custom @Component
for the @Singleton
scope anymore.
While this is true, there’s still something you need to take care of. To understand what, open di/AppComponent.kt and look inside:
@Singleton
@Component(modules = [AppModule::class, FeatureModule::class])
interface AppComponent {
fun featureComp(): FeatureComponent
}
In this code, you’re not just defining a @Component
for the @Singleton
scope, you’re also telling Dagger which bindings this @Component
should manage. Remember that a binding is a way to tell Dagger which specific class to instantiate to satisfy a dependency for a given type.
AppComponent
also tells Dagger which objects have the same lifecycle as the Application and so, with @Singleton
, the same scope.
Dagger defines bindings in @Module
. In the previous code, you see that AppComponent
provides references for the objects in the AppModule
.
FeatureModule
is only there because of the FeatureComponent
subcomponent, which you’ll fix later.
Open di/AppModule.kt and look at the following code:
@Module
abstract class AppModule {
@Binds
abstract fun provideNewsRepository(
newsRepository: MemoryNewsRepository
): NewsRepository
}
This code provides a binding for the NewsRepository
implementation.
Before deleting di/AppComponent.kt, you have find another way to give Dagger the same information, which it needs to instantiate the proper class for MemoryNewsRepository
.
For this, Hilt follows a very simple approach. Instead of creating a @Component
with a reference to the @Module
containing the bindings it needs, it allows you to tell Dagger which @Components
the bindings in a @Module
belong to.
You do this using @InstallIn
and applying a simple change: adding the following line to AppModule
:
@Module
@InstallIn(ApplicationComponent::class) // HERE
abstract class AppModule {
@Binds
abstract fun provideNewsRepository(
newsRepository: MemoryNewsRepository
): NewsRepository
}
Using @InstallIn(ApplicationComponent::class)
in AppModule.kt, you’re telling Dagger that all the bindings in AppModule
will be part of the dependency graph for the ApplicationComponent
that Hilt provides automatically.
Note how the @InstallIn
annotation needs a parameter, which is the name of the component where you should add the related binding.
After this change, you can finally delete AppComponent.kt without any fear. :]
Refactoring @FeatureScope
The next step is to repeat the process for the objects with @FeatureScope
. @FeatureScope
is a custom annotation in RWNEws, which represents a scope equivalent to FragmentScope in Hilt.
Open di/FeatureModule.kt and add the following annotation to the class header:
@InstallIn(FragmentComponent::class)
The resulting code should look like the following:
@Module
@InstallIn(FragmentComponent::class) // HERE
abstract class FeatureModule {
// ...
}
With the @InstallIn
annotation, you’re adding FeatureModule
‘s’ bindings to the FragmentComponent with FragmentScope.
Do the same for di/StatsModule.kt. Open this file and add the same annotation to the class header. The resulting code becomes:
@Module
@InstallIn(FragmentComponent::class) // HERE
class StatsModule {
@Provides
@ElementsIntoSet
fun provideNewsStats(): Set<NewsStats> = setOf(
LengthNewsStats()
)
}
Finally, open thirdparty/ThirdPartyStatsModule.kt and repeat the same annotation as follows:
@Module
@InstallIn(FragmentComponent::class) // HERE
class ThirdPartyStatsModule {
@Provides
@IntoSet
fun provideWordsCountNewsStats(): NewsStats = WordCountNewsStats()
}
Now, delete FeatureComponent.kt and FeatureScope.kt since you don’t need them anymore.
Understanding Entry Points
In the previous sections, you helped Hilt create the dependency graph for objects with different scopes. Now, you need a way to inject those objects into Activities, Fragments or any other Android standard components.
To do this, Hilt uses entry points, which function as an interface between the dependency graph and the object destination of the injection. Every time you need to tag a class as the target of an injection, Hilt requires you to annotate it with @AndroidEntryPoint
.
@AndroidEntryPoint
s. For instance, ContentProvider
isn’t supported at the moment. On the other hand, Hilt supports Fragment
components that, in theory, aren’t formally Android Standard Components, since you don’t define them in AndroidManifest.xml
.
Using @AndroidEntryPoint With Activities
RWNEws is a simple app that contains a couple of Fragment
s and a simple Activity
as their container. Open ui/MainActivity.kt and look at the following code:
// 1
typealias FeatureComponentProvider = Provider<FeatureComponent>
class MainActivity : AppCompatActivity(), FeatureComponentProvider {
// 2
lateinit var featureComp: FeatureComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.anchor, NewsListFragment())
.commit()
featureComp = (applicationContext as InitApp).appComp().featureComp() // 3
}
}
// 4
override fun get(): FeatureComponent = featureComp
}
Here, you:
- Create
codeFeatureComponentProvider
as an alias ofProvider<FeatureComponent>
. - Define
featureComp
as a property containing theFeatureComponent
you need to return asFeatureComponentProvider
. - Create the instance of
FeatureComponent
to save infeatureComp
. - Provide the implementation of
get()
that theMainActivity
must provide when it implementsFeatureComponentProvider
.
At this point, you might wonder why you’re looking at MainActivity
if there’s nothing to inject. Remember that Hilt provides a @Component
hierarchy and you don’t need to create them anymore. Therefore, you need to replace the previous code with the following:
@AndroidEntryPoint // HERE
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.anchor, NewsListFragment())
.commit()
}
}
}
Wait! All the @Component
creation code is gone, so why do you need to annotate MainActivity
with @AndroidEntryPoint
? You have to do it because MainActivity
is going to host Fragment
s that will be @AndroidEntryPoint
as well, and Hilt needs to know that.