17.
Hilt — Dagger Made Easy
Written by Massimo Carli
In the previous chapter, you learned how Dagger Android works and Android needs a special library to use Dagger. You learned that Dagger can’t instantiate Android standard components because their lifecycle is the responsibility of the Android system, which works as a container. Dagger Android has not been very successful but it taught Google lessons that they used when making architectural decisions for Hilt.
Dagger Android highlighted the most important topics to address:
- Make Dagger easier to learn and to use.
- Standardize the way developers create
@Scopes and apply them to@Components. - Make implementing tests easier.
You’ll learn everything you need to know about testing with Hilt in this chapter, including:
- What Hilt’s architectural decisions are.
- How to install Hilt in your app.
- What
@AndroidEntryPointis and how to use it. - What a Hilt
@Moduleis and how it differs from Dagger’s. - How to deal with different
@Scopes with Hilt. - Which tools Hilt provides to handle common use cases.
Of course, you’ll put all these things to work in the Busso App.
Note: At this time, Hilt is still in alpha release. Things might change in future versions of the library.
Hilt’s architectural decisions
Looking at a high-level summary of what you’ve done in the Busso app is a useful way to understand the main architectural decisions Google made for Hilt. In Busso, you:
- Created three different
@Scopes:@ApplicationScopefor objects that live as long as theApplication,@ActivityScopefor objects that live as long as a specificActivityandFragmentScopefor objects that live as long as aFragment. - Implemented a different
@Componentor@Subcomponentfor each@Scope. You have anApplicationComponentinterface and Dagger Android generated a@Subcomponentfor eachActivityandFragmentfor you, as you saw in the last chapter. - Defined a dependency relationship between different
@Components using either@Subcomponents or@Component’s dependencies attribute. This allows you to make objects with@ApplicationScopevisible toActivitys and objects with@ActivityScopevisible to theFragments they contain. - Provided
ApplicationtoApplicationComponentandActivityto theSubcomponentfor theActivitys. You also added these objects to the corresponding dependency graph. - Defined different
@Modules to tell Dagger how to bind a specific class to a given type.
To do this, you had to learn many different concepts and several ways to give Dagger the information it needs. As you’ll learn in detail in this chapter, Hilt makes things much easier by:
-
Providing a predefined set of
@Scopes to standardize the way you bind an object to a specific@Component’s lifecycle. -
Doing the same for
@Components. Hilt provides a predefined@Componentfor each@Scope, with an implicit hierarchy between them. -
Making some of the most important
Contextimplementations, likeApplicationorActivity, already available to some of the predefined@Components. -
Defining a new way to bind a
@Moduleto a specific@Component. Now you install a@Modulein one or more@Components.
This will become clearer when you start using Hilt for your Busso App.
Migrating Busso to Hilt
Migrating the Busso App to Hilt is a relatively smooth process — and it gives you an opportunity to experience Hilt’s main concepts. You’ll dive into those concepts more deeply later in the tutorial.
For your migration, you need to:
- Install Hilt dependencies and plugins.
- Enable Hilt in Busso’s
Application. - Use a predefined
@Scopefor theApplicationComponent. - Migrate
Activitys to Hilt using@AndroidEntryPoint. - In the same way, migrate
Fragments to Hilt. - Install each
@Modulein the right@Componentusing@InstallIn. - Build, run and enjoy Busso.
Now — it’s time to write some code!
Note: As you already know, refactoring with Dagger means that you usually need to complete all the configurations before you can successfully build and run the app. This is also true in this case. However, it’s good practice to try building the app at each step, anyway, allowing Dagger to generate what it can.
Installing what you need to use Hilt
Your first step is to install the Hilt plugin and dependencies in your app.
Start by opening the Busso App project in the starter folder of the materials for this chapter. This is the final project from the previous chapter, which uses Dagger Android. When you open the project, you’ll have this structure:
Now, open build.gradle in the main folder for the project, as in Figure 17.2:
Then, apply the following changes:
buildscript {
ext.kotlin_version = "1.4.20"
ext.hilt_version = "2.28-alpha" // 1
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" // 2
}
}
// ...
In this Gradle file, you:
- Include the definition of
ext.hilt_versionwith the value of the latest version of Hilt. - Add
classpathfor Hilt’s Gradle plugin.
Now, you have to apply the Hilt plugin to build.gradle for the modules that need it. For starters, open build.gradle from app, as in Figure 17.3:
Now, apply the following changes:
plugins {
id 'com.android.application'
id 'kotlin-android'
id "kotlin-kapt"
id "dagger.hilt.android.plugin" // 1
}
apply from: '../versions.gradle'
android {
// ...
compileOptions { // 2
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
// ...
}
dependencies {
// ...
// Hilt dependencies
implementation "com.google.dagger:hilt-android:$hilt_version" // 3
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" // 4
// ...
}
There are some interesting things to note in the Gradle file above. Here, you:
- Add the plugin for Hilt. The plugin isn’t necessary, and Hilt works without it, but it makes the developer experience better. For instance, it executes some bytecode transformations to make the code more IDE-friendly. This helps auto-complete the code without messing with auto-generated code. It also makes some code simpler. You’ll see this later, when you work with
@HiltAndroidApp. - Specify using Java 1.8, which Hilt requires. Busso already has the proper configuration in
compileOptions. - Add the dependency to the Hilt library.
- Indicate the specific library
kaptuses for Hilt’s annotation processor.
Now, sync the Busso project with the Gradle file and you’re ready to use Hilt in your app. You just need a way to activate the Hilt plugin by providing an entry point to the Hilt world. You’ll do this by using Application. In Busso, you’ll find this class in Main.kt.
Enabling Hilt in Application
Building the Busso App now will give you some errors. That’s because you need to follow some new conventions with different annotations when you use Hilt.
Your next step in migrating Busso to Hilt is to define the entry point for the entire dependency graph. You must do this in your app’s Application implementation. If your app doesn’t have an Application implementation, you need to create one.
Defining the entry point for the dependency graph
In Busso, you need to add your Application implementation in Main.kt in app. Open Main.kt and change the content of the file to this:
@HiltAndroidApp // 1
class Main : Application() // 2
Hey, where’s all the code? Don’t worry, one of the main benefits of Hilt is that you don’t need all the previous code. Here, you:
- Used
@HiltAndroidAppto tell Dagger that this is the entry point for Busso’s dependency graph. - Created
Mainas a simpleApplication.
Adding Application to the dependency graph
That looks good, but you might wonder if something’s missing. In the previous implementation of Main, you had the following:
DaggerApplicationComponent
.factory()
.create(this, BussoConfiguration)
There, you created an instance of ApplicationComponent by passing the reference to:
-
Application. -
NetworkingConfiguration.
That’s how you added Application and NetworkingConfiguration to ApplicationComponent’s dependency graph. That made them automatically available for injection into any other object of the app.
With Hilt, on the other hand, you:
- Don’t need to do anything for
Application. Hilt guarantees it’s automatically added to the dependency graph forApplicationComponent, making it available for injection into any other object. - Need to find a different way to tell Dagger which
NetworkingConfigurationto use and how to add it to the dependency graph forApplicationScope.
To address the second point, you need to tell Dagger:
- How to bind
BussoConfigurationtoNetworkingConfiguration. - To add that implementation to the dependency graph for
ApplicationComponent.
Adding a @Module
You already know how to solve the first point: you need a @Module. Open ApplicationModule.kt in di and add the following definition:
@Module(
includes = [
LocationModule::class,
NetworkModule::class,
AndroidSupportInjectionModule::class
]
)
object ApplicationModule {
@Provides
fun provideNetworkingConfiguration(): NetworkingConfiguration = // HERE
BussoConfiguration
}
Here, you tell Dagger that whenever it needs NetworkingConfiguration, it’ll use BussoConfiguration.
Binding @Module’s objects to ApplicationComponent
Now, you need to bind the objects in this @Module to ApplicationComponent. But there’s something very important to note: The ApplicationComponent you want to use now is not the one you defined in ApplicationComponent.kt in di.
Be brave and delete ApplicationComponent.kt, then apply the following change to ApplicationModule.kt:
@Module(
includes = [
LocationModule::class,
NetworkModule::class,
AndroidSupportInjectionModule::class
]
)
@InstallIn(ApplicationComponent::class) // HERE
object ApplicationModule {
@Provides
fun provideNetworkingConfiguration(): NetworkingConfiguration =
BussoConfiguration
}
You’ll see this in more detail later. At the moment, you’re using @InstallIn to tell Dagger that you want all the bindings you have in ApplicationModule.kt to be a part of the dependency graph for ApplicationComponent.
But which ApplicationComponent is that if you just deleted ApplicationComponent.kt? As you read earlier, Hilt has a set of predefined @Components — and ApplicationComponent, which you find in dagger.hilt.android.components — is one of those.
In this section, you did something very simple but powerful. You told Dagger:
- To use
Mainas the entry point for Busso’s dependency graph. You did this using@HiltAndroidApp. - That you want to use
BussoConfigurationas theNetworkingConfigurationimplementation. - That all the definitions you made in
ApplicationModulewill be part of the dependency graph for the predefinedApplicationComponent. You did this using@InstallIn, which you’ll learn more about later.
You also learned that Hilt makes Application automatically available to the dependency graph for the ApplicationComponent — which also makes it available to all the other predefined Hilt @Components.
Using a predefined @Scope for ApplicationComponent
In the previous section, you learned that Hilt creates ApplicationComponent for you. You just need to tell it which objects to put into its dependency graph. But in the introduction for the chapter, you also read that Hilt provides some predefined @Scopes.
You’ll learn about all the available @Scopes and @Components later. At the moment, it’s important to know that the @Scope for ApplicationComponent is @Singleton. To migrate from the existing custom @Scopes in the libs.di.scopes module to Hilt’s, make the following changes:
- Delete ApplicationScope.kt, ActivityScope.kt and FragmentScope.kt from libs.di.scopes.
- Add the Hilt dependencies in build.gradle for libs.di.scopes.
- Fix the dependent modules using the Hilt
@Scopesinstead of the ones you just deleted.
Note: Here, you’re using libs.di.scopes as the module containing the dependencies for Hilt. This makes things easier to explain but, of course, you could also completely delete libs.di.scopes and fix all the dependencies in the other modules accordingly.
Note: If you don’t want to remove your custom
@Scopes, Hilt provides you the@AliasOfannotation, which allows you to use your custom@Scopeannotation in place of the ones Hilt provides. You’ll see an example of this later.
After step 2, build.gradle for libs.di.scopes should look like this:
plugins {
id 'com.android.library'
id 'kotlin-android'
id "kotlin-kapt"
id "dagger.hilt.android.plugin"
}
apply from: '../../../versions.gradle'
android {
compileSdkVersion compile_sdk_version
buildToolsVersion build_tool_version
}
dependencies {
api "javax.inject:javax.inject:$javax_annotation_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
Of course, now you need to replace all the existing custom @Scopes with Hilt’s. But don’t worry about that, yet. At the moment, just replace the previous @ApplicationScope with @Singleton. You can find all the occurrences with a simple search:
After you replaced all the @ApplicationScopes with @Singletons, you can start migrating Busso’s Activitys.
Migrating Activitys to Hilt using @AndroidEntryPoint
In the previous section, you fixed most of the dependencies related to ApplicationComponent by using @Singleton. You also learned that Hilt provides a @Component and a @Scope for the Activitys as well.
This is true. Hilt provides ActivityComponent and @ActivityScoped. In Busso, you need to:
- Delete the existing ActivityBindingModule.kt from di.
- Tell Dagger that the objects in ActivityModule.kt should be installed in
ActivityComponent. - Replace the custom
@ActivityScopewith@ActivityScoped(note the ending d).
After deleting ActivityBindingModule.kt, open ActivityModule.kt in di.activities and change it like this:
@Module(
includes = [
NavigatorModule::class
]
)
@InstallIn(ActivityComponent::class) // HERE
interface ActivityModule {
// ...
}
Here, you’re just using @InstallIn to install this @Module’s bindings in ActivityComponent.
Now, search for @ActivityScope and replace all the occurrences with @ActivityScoped, as in Figure 17.5:
After replacing those occurrences, there’s one more step to complete your work with Activitys: You need to execute the injection in MainActivity and SplashActivity.
To do this, Hilt defines @AndroidEntryPoint, which allows you to tag a standard Android component as a possible dependency target.
Later, you’ll see a detailed list of all the classes Hilt’s @AndroidEntryPoint annotation supports. At the moment, you just need to open SplashActivity.kt in ui.view.splash and change it like this:
@AndroidEntryPoint // 1
class SplashActivity : AppCompatActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// AndroidInjection.inject(this) // 2 TO DELETE
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
splashViewBinder.init(this)
}
// ...
}
In this case you:
- Add
@AndroidEntryPoint. - Remove the
AndroidInjection.inject()invocation, which you don’t need anymore.
In the same way, open MainActivity.kt in ui.view.main and apply the following changes
@AndroidEntryPoint // 1
class MainActivity : AppCompatActivity() { // 2
// ...
}
In MainActivity’s case, you:
- Added
@AndroidEntryPoint. - Restored
AppCompatActivityas the parent class forMainActivityin place ofDaggerAppCompatActivity. Note that this implicitly removes theAndroidInjection.inject()invocation.
At this point, it’s important to note that:
- All the bindings you defined in
ActivityModuleare now available inActivityComponent’s dependency graph. -
MainActivityandSplashActivityare dependency targets for the@ActvityScopedobjects inActivityComponent. - The
@ActivityScopeddependency targets can also access objects with@Singletonscope. Hilt implicitly manages dependencies between@Components. - Likewise, objects in the dependency graph for
@ApplicationComponentaccessApplication, and all objects in the dependency graph forActivityComponentaccessActivity.
Now, it’s time to migrate Busso’s Fragments to Hilt. By now, you probably know how to do that. :]
Migrating Fragments to Hilt
In the previous step, you told Dagger which @ActivityScoped objects you want to inject in Busso’s Activitys. Now, you have to do the same for Fragments. In this case, you’ll need a @Scope and a @Component for Fragments that Hilt provides with:
- @FragmentScoped
- FragmentComponent
To use them, you just need to:
- Delete FragmentBindingModule.kt in di.
- Use
@InstallInto installFragmentModulein di.fragments inFragmentComponent. - Use
@AndroidEntryPointto tagBusStopFragmentandBusArrivalFragmentas dependency targets for injection. - Replace the existing
@FragmentScopewith@FragmentScoped(again, watch the final d).
After deleting FragmentBindingModule.kt, open FragmentModule.kt in di.fragments and change the FragmentModule definition to this:
@Module(includes = [InformationPluginEngineModule.FragmentBindings::class])
@InstallIn(FragmentComponent::class) // HERE
interface FragmentModule {
// ...
}
Now, open BusStopFragment.kt in ui.view.busstop and apply the following changes:
@AndroidEntryPoint // 1
class BusStopFragment : Fragment() {
// ...
/* START REMOVE
override fun onAttach(context: Context) { // 2
AndroidSupportInjection.inject(this)
super.onAttach(context)
} END REMOVE
*/
// ...
}
Here, you need to do two very important things:
- Add
@AndroidEntryPointto tell Hilt thatBusStopFragmentis a dependency target. - Remove the existing
inject()invocation. Hilt will inject the objects you require intoBusStopFragmentfor you.
Now, you need to do the same for BusArrivalFragment. Open BusArrivalFragment.kt in ui.view.busarrival and apply the same change:
@AndroidEntryPoint // 1
class BusArrivalFragment : Fragment() {
// ...
/* START REMOVE
override fun onAttach(context: Context) { // 2
AndroidSupportInjection.inject(this)
super.onAttach(context)
} END REMOVE
*/
// ...
}
As your final step, you need to replace @FragmentScope with Hilt’s @FragmentScoped, just as you did for @ActivityScoped and @Singleton. You can see this in Figure 17.6:
Great job! You’re almost done migrating Busso to Hilt. Now, you just need to check that you installed all the @Modules in the right @Components and do a bit of cleaning up.
Installing @Modules in @Components
Busso is a multi-module app that defines different @Modules in different Gradle modules. To complete the migration, you need to:
- Install
NetworkModule,InformationPluginEngineModuleandInformationSpecsModulein@ApplicationComponent. - Fix the injection of the
Navigatorimplementation.
These steps should be quite straightforward now. Open NetworkModule.kt in network and use @InstallIn to install it in the ApplicationComponent:
@Module(
includes = [
NetworkingModule::class
]
)
@InstallIn(ApplicationComponent::class) // HERE
object NetworkModule {
// ...
}
Open ApplicationModule.kt in di and add the following definition:
@Module(
includes = [
LocationModule::class,
NetworkModule::class,
AndroidSupportInjectionModule::class,
InformationPluginEngineModule::class // HERE ADD
]
)
@InstallIn(ApplicationComponent::class)
object ApplicationModule {
// ...
}
In this case, it’s important to note how InformationPluginEngineModule doesn’t need to use @InstallIn itself. Hilt will install InformationPluginEngineModule in ApplicationComponent as part of ApplicationModule.
Now, you also need to install InformationSpecsModule by adding the following code in InformationSpecsModule.kt in plugins:
@Module(
includes = [
WhereAmIModule::class,
WeatherModule::class
]
)
@InstallIn(ApplicationComponent::class) // HERE
object InformationSpecsModule
Fixing the Navigator injection
Build the app now and you’ll get an error related to the Navigator implementation. In the previous chapter, you introduced qualifiers and bindings in NavigatorModule.kt in di.navigator. The good news is — you don’t need these anymore. To fix the errors, you need to:
- Delete NavigatorModule.kt in di.navigator.
- Restore the use of
NavigationModuleinActivityModulein place of theNavigatorModuleyou just removed. - Delete every use of the
@Namedqualifier forNavigator.
So delete NavigatorModule.kt, then open ActivityModule.kt in di.activities and apply the following change:
@Module(
includes = [
NavigationModule::class // HERE
]
)
@InstallIn(ActivityComponent::class)
interface ActivityModule {
// ...
}
Here, you replaced NavigatorModule with NavigationModule. Next, delete the @Named("Main") and @Named("Splash") qualifiers in MainPresenterImpl, BusStopListPresenterImpl and SplashViewBinderImpl.
Here’s how these classes will look now:
class MainPresenterImpl @Inject constructor(
private val navigator: Navigator // HERE
) : MainPresenter {
// ...
}
@FragmentScoped
class BusStopListPresenterImpl @Inject constructor(
private val navigator: Navigator, // HERE
private val locationObservable: Observable<LocationEvent>,
private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(),
BusStopListPresenter {
// ...
}
class SplashViewBinderImpl @Inject constructor(
private val navigator: Navigator // HERE
) : SplashViewBinder {
// ...
}
For your last step, restore the encapsulation for libs.ui.navigation using the internal visibility modifier in the NavigatorImpl.kt file, like this:
// HERE
internal class NavigatorImpl(private val activity: Activity) : Navigator {
// ...
}
You did it! You can now successfully build and run Busso, ending up with what you see in Figure 17.7:
It’s now time for a short summary.
Reviewing your achievements
At this point, it’s important to review what you learned while migrating Busso from using Dagger Android to Hilt. Here’s what you discovered:
- Hilt provides a predefined set of
@Scopes and@Components that already implement a dependency relationship. For instance, all the objects with@Singletonscope are visible to the@ActivityScopedobjects. - For
ApplicationComponent, you use@HiltAndroidAppin your app’sApplication. - You use
@InstallInto make the bindings in a specific@Moduleavailable to one or more@Components. - Some of the
@Components already provideContextimplementations, likeApplicationorActivity. For instance, theNavigationModuleneeds anActivity, which Hilt provides automagically.
You won’t believe it but these are the main things you need to know to use Hilt in your app. Of course, you only saw a few of the @Components and @Scopes available. Now, you’ll get a quick overview of everything Hilt provides.
Hilt’s main APIs
While migrating Busso to Hilt, you learned that the main concepts you need to know are:
- Entry point and
@AndroidEntryPoint. - Generated
@Components and@Scopes.
You already know how to use them. In the following sections, you’ll just learn what’s available.
Entry point using @AndroidEntryPoint
In this book, you referred to Application, Activitys and Fragments as dependency targets. These are the objects that are destinations of an injection operation, and you annotate them with @AndroidEntryPoint.
Application has a special meaning, so you use @HiltAndroidApp, instead. In Busso, you did this with Application, Activitys and Fragments but you can do the same with all the following classes:
-
Application, by using@HiltAndroidApp -
Activitys that extendandroidx.activity.ComponentActivity -
Fragments that extendandroidx.Fragment ViewServiceBroadcastReceiver
You might notice that this list doesn’t match the Android standard components. It adds Fragments and Views but ContentProvider is missing. You might wonder when and how you could support more classes.
In theory, you should be able to use the existing entry points, but sometimes you need to handle third-party libraries or components that Hilt doesn’t support yet.
As you’ll learn in the next chapter, Hilt provides you with the APIs to create your own entry point, component and scope and integrate them with the ones you already have.
Using predefined @Components & @Scopes
In terms of @Components and @Scopes, Hilt provides what’s shown in Figure 17.8:
In this dependency diagram, you find ApplicationComponent and ActivityComponent, along with their related @Scopes: @Singleton and @ActivityScope. Looking at the other @Components, it’s worth saying more about:
-
ActivityRetainedComponentwith@ActivityRetainedScoped -
ViewWithFragmentComponentwith@ViewScoped
ActivityRetainedComponent is similar to ActivityComponent but it uses a different lifecycle. It allows the bindings to survive configuration changes.
In this case, you need to take care that you don’t have Activity as default binding, but rather Application, as in ApplicationComponent.
ViewWithFragmentComponent also allows you to optimize the lifecycle of Views when part of the View hierarchy is attached to a Fragment.
Finally, here are the default bindings for each predefined @Component:
-
ApplicationComponent:
Application -
ActivityRetainedComponent:
Application -
ActivityComponent:
ApplicationandActivity -
FragmentComponent:
Application,ActivityandFragment -
ViewComponent:
Application,ActivityandView -
ViewWithFragmentComponent:
Application,Activity,FragmentandView -
ServiceComponent:
ApplicationandService
When you need to decide which @Scope and @Component to use in your bindings, you need to consider:
- The lifecycle of the objects you inject.
- Which default bindings the specific
@Componentprovides.
After that, you can apply the rules you just learned with the Busso App.
Hilt utility APIs
Hilt provides some utility APIs, which help migrate existing apps. The main ones are:
@AliasOf@ApplicationContext@ActivityContext
You’ll see a simple example for each of those next.
Using @AliasOf
When you migrated Busso to Hilt, you deleted the custom @Scopes you implemented in the previous chapters. You deleted @ApplicationScope, @ActivityScope and @FragmentScope in favor of the predefined @Singleton, @ActivityScoped and @FragmentScoped.
Suppose you don’t like @Singleton and want to keep the same naming convention by using @ApplicationScoped, instead.
You’d create a new file named ApplicationScoped.kt in libs.di.scopes with the following code:
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@AliasOf(Singleton::class) // HERE
annotation class ApplicationScoped
With this definition, you tell Dagger that @ApplicationScoped is just an alias for @Singleton. To prove this, just search for @Singleton and replace all the instances with the new @ApplicationScoped. After that, build and run the app as usual and it will work.
Of course, the best use case for @AliasOf is when you already have a lot of occurrences for a custom @Scope and you want to avoid replacing too much code.
Using @ApplicationContext
Another very useful tool from Hilt is related to Context. You’ve seen that some of the predefined @Components have an Application for default bindings, while others have an Activity. As you know, they’re both implementations of the Android Context and if you want to distinguish one from the other, you use a qualifier. In this specific case, however, Hilt already provides the qualifier you need.
For instance, open LocationModule.kt in libs.location.rx and look at the following code:
@Module
class LocationModule {
@ApplicationScoped
@Provides
fun provideLocationManager(application: Application): LocationManager = // HERE
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// ...
}
Here, you used Application to get a reference to LocationManager. In this case, you really just need a Context and Hilt allows you to use that by applying the following change:
@Module
class LocationModule {
@ApplicationScoped
@Provides
fun provideLocationManager(
@ApplicationContext context: Context // HERE
): LocationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// ...
}
Here, you replaced Application with Context, using @ApplicationContext to tell Dagger that the Context you want is same as Application.
Using @ActivityContext
You can do the same when you need the Context of an Activity. In that case, you also have the opportunity to improve Busso’s resource management.
Open LocationModule.kt in libs.location.rx and replace the current code with the following:
// 1
class LocationModule {
@Module
object ApplicationBindings { // 2
@ApplicationScoped
@Provides
fun provideLocationManager(
@ApplicationContext context: Context
): LocationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@Module
object ActivityBindings { // 3
@ActivityScoped // 4
@Provides
fun providePermissionChecker(
@ActivityContext context: Context // 5
): GeoLocationPermissionChecker =
GeoLocationPermissionCheckerImpl(context)
@Provides
@ActivityScoped // 4
fun provideLocationObservable(
locationManager: LocationManager,
permissionChecker: GeoLocationPermissionChecker
): Observable<LocationEvent> = provideRxLocationObservable(locationManager, permissionChecker)
}
}
As you see, this makes a few changes to LocationModule. In particular:
-
LocationModuleis not a@Moduleanymore — it’s just a container for a few other@Modules for bindings with different scopes. -
ApplicationBindingsis a@Modulethat contains the bindings with@ApplicationScopedor@Singleton. -
ActivityBindingsis the@Modulecontaining the bindings with@ActivityScoped. - You now use
@ActivityScopedfor theGeoLocationPermissionCheckerandObservable<LocationEvent>, whose optimal lifecycle is the same asActivity. - Use
@ActivityContextto tell Dagger that theContextyou require forGeoLocationPermissionCheckerisActivity.
Updating where you had LocationModule
Now, you need to fix the places where you used LocationModule. Open ApplicationModule.kt in di and add the following definition:
@Module(
includes = [
LocationModule.ApplicationBindings::class, // HERE
NetworkModule::class,
AndroidSupportInjectionModule::class,
InformationPluginEngineModule::class
]
)
@InstallIn(ApplicationComponent::class)
object ApplicationModule {
// ...
}
Then, open ActivityModule.kt in di.activities and add the following definition:
@Module(
includes = [
NavigationModule::class,
LocationModule.ActivityBindings::class // HERE
]
)
@InstallIn(ActivityComponent::class)
interface ActivityModule {
// ...
}
Now, you can build and run Busso as usual and verify it works.
Key points
- The main goal of Hilt is to make Dagger easier to learn and to use.
- Hilt provides a predefined set of
@Components and@Scopes. - Using
@InstallIn, you install the binding of a@Modulein a specific@Component. - Each predefined Hilt
@Componentprovides a set of default bindings. - There’s an implicit hierarchy between Hilt’s predefined
@Components. -
@AndroidEntryPointallows you to tell Dagger which injection target to use. -
@AliasOfsimplifies the migration of existing apps to Hilt. - Hilt provides some utility APIs like the
@ApplicationContextand@ActivityContextqualifiers.
Congratulations! In this chapter, you learned the main concepts of Hilt, the new framework with the goal of simplifying Dagger usage in Android apps. However, Hilt is more than that. In the next chapter, you’ll learn more about it — in particular, how to use customize it and how to use it with Android architecture components.
See you there!