Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

17. Hilt — Dagger Made Easy
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 @AndroidEntryPoint is and how to use it.
  • What a Hilt @Module is 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:

  1. Created three different @Scopes: @ApplicationScope for objects that live as long as the Application, @ActivityScope for objects that live as long as a specific Activity and FragmentScope for objects that live as long as a Fragment.
  2. Implemented a different @Component or @Subcomponent for each @Scope. You have an ApplicationComponent interface and Dagger Android generated a @Subcomponent for each Activity and Fragment for you, as you saw in the last chapter.
  3. Defined a dependency relationship between different @Components using either @Subcomponents or @Component’s dependencies attribute. This allows you to make objects with @ApplicationScope visible to Activitys and objects with @ActivityScope visible to the Fragments they contain.
  4. Provided Application to ApplicationComponent and Activity to the Subcomponent for the Activitys. You also added these objects to the corresponding dependency graph.
  5. 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:

  1. Providing a predefined set of @Scopes to standardize the way you bind an object to a specific @Component’s lifecycle.

  2. Doing the same for @Components. Hilt provides a predefined @Component for each @Scope, with an implicit hierarchy between them.

  3. Making some of the most important Context implementations, like Application or Activity, already available to some of the predefined @Components.

  4. Defining a new way to bind a @Module to a specific @Component. Now you install a @Module in 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.

Installing what you need to use Hilt

Your first step is to install the Hilt plugin and dependencies in your app.

Figure 17.1 — Initial Project Structure
Vopuge 58.6 — Adeveob Jkecadg Mmgarnuze

Figure 17.2 — build.gradle in the root of the Busso project
Quxiko 64.6 — feibm.jgikmo eq kmo neup ot xbi Jaqro wnomimq

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
  }
}
// ...
Figure 17.3 — build.gradle for the app module
Guhaco 35.3 — kuewn.wropva suw wsa ucs kamefe

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
  // ...
}

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.

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

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)

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
}

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.

@Module(
  includes = [
    LocationModule::class,
    NetworkModule::class,
    AndroidSupportInjectionModule::class
  ]
)
@InstallIn(ApplicationComponent::class) // HERE
object ApplicationModule {

  @Provides
  fun provideNetworkingConfiguration(): NetworkingConfiguration =
    BussoConfiguration
}

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.

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"
}
Figure 17.4 — @ApplicationScope usages
Liwula 55.2 — @OxfnuxahuibPlexe ovudan

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.

@Module(
  includes = [
    NavigatorModule::class
  ]
)
@InstallIn(ActivityComponent::class) // HERE
interface ActivityModule {
  // ...
}
Figure 17.5 — @ActivityScope usages
Zisima 33.8 — @OddilunvScaha odurox

@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)
  }  
  // ...  
}
@AndroidEntryPoint // 1
class MainActivity : AppCompatActivity() { // 2
  // ...
}

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:

@Module(includes = [InformationPluginEngineModule.FragmentBindings::class])
@InstallIn(FragmentComponent::class) // HERE
interface FragmentModule {
  // ...
}
@AndroidEntryPoint // 1
class BusStopFragment : Fragment() {
  // ...
  /* START REMOVE
  override fun onAttach(context: Context) { // 2
    AndroidSupportInjection.inject(this)
    super.onAttach(context)
  } END REMOVE
  */

  // ...
}
@AndroidEntryPoint // 1
class BusArrivalFragment : Fragment() {
  // ...
  /* START REMOVE
  override fun onAttach(context: Context) { // 2
    AndroidSupportInjection.inject(this)
    super.onAttach(context)
  } END REMOVE
  */
  // ...
}
Figure 17.6 — @FragmentScope usages
Xoqihu 77.0 — @GyoyriflThani ekofel

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:

@Module(
  includes = [
    NetworkingModule::class
  ]
)
@InstallIn(ApplicationComponent::class) // HERE
object NetworkModule {
  // ...
}
@Module(
  includes = [
    LocationModule::class,
    NetworkModule::class,
    AndroidSupportInjectionModule::class,
    InformationPluginEngineModule::class // HERE ADD
  ]
)
@InstallIn(ApplicationComponent::class)
object ApplicationModule {
  // ...
}
@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:

@Module(
  includes = [
    NavigationModule::class // HERE
  ]
)
@InstallIn(ActivityComponent::class)
interface ActivityModule {
  // ...
}
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 {
  // ...
}
// HERE
internal class NavigatorImpl(private val activity: Activity) : Navigator {
  // ...
}
Figure 17.7 — The Busso App
Huxime 65.1 — Jma Fotla Inb

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’s main APIs

While migrating Busso to Hilt, you learned that the main concepts you need to know are:

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.

Using predefined @Components & @Scopes

In terms of @Components and @Scopes, Hilt provides what’s shown in Figure 17.8:

Figure 17.8 — Predefined @Components and @Scopes
Jewuqa 68.5 — Cmolunofap @Xobromabrx icy @Wwibay

Hilt utility APIs

Hilt provides some utility APIs, which help migrate existing apps. The main ones are:

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.

@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@AliasOf(Singleton::class) // HERE
annotation class ApplicationScoped

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.

@Module
class LocationModule {
  @ApplicationScoped
  @Provides
  fun provideLocationManager(application: Application): LocationManager = // HERE
    application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // ...
}
@Module
class LocationModule {
  @ApplicationScoped
  @Provides
  fun provideLocationManager(
    @ApplicationContext context: Context // HERE
  ): LocationManager =
    context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // ...
}

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.

// 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)
  }
}

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 {
  // ...
}
@Module(
  includes = [
    NavigationModule::class,
    LocationModule.ActivityBindings::class // HERE
  ]
)
@InstallIn(ActivityComponent::class)
interface ActivityModule {
  // ...
}

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 @Module in a specific @Component.
  • Each predefined Hilt @Component provides a set of default bindings.
  • There’s an implicit hierarchy between Hilt’s predefined @Components.
  • @AndroidEntryPoint allows you to tell Dagger which injection target to use.
  • @AliasOf simplifies the migration of existing apps to Hilt.
  • Hilt provides some utility APIs like the @ApplicationContext and @ActivityContext qualifiers.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now