This is the first article in the MAD Skills series about Hilt! In this article, we’ll explore the importance of dependency injection (DI) for applications and Jetpack’s recommended Android DI solution, Hilt.

If you prefer to see this in video, you can check it out here.

In Android applications, you can lay the foundation for good application architecture by following the principles of dependency injection. It’s easy to reuse code, easy to refactor, easy to test! For more on the benefits of DI, see dependency injection in Android.

When creating instances of classes in a project, you can manually work with the dependency diagram by supplying and passing the required dependencies.

But doing it manually each time adds template code and is error prone. Taking a ViewModel in the IOsched project (Google I/O open source application) as an example, can you imagine the amount of code required to create a FeedViewModel dependency and pass it around?

class FeedViewModel(
    private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
    loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
    private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
    getTimeZoneUseCase: GetTimeZoneUseCase,
    getConferenceStateUseCase: GetConferenceStateUseCase,
    private val timeProvider: TimeProvider,
    private val analyticsHelper: AnalyticsHelper,
    private val signInViewModelDelegate: SignInViewModelDelegate,
    themedActivityDelegate: ThemedActivityDelegate,
    private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
    FeedEventListener,
    ThemedActivityDelegate by themedActivityDelegate,
    SignInViewModelDelegate by signInViewModelDelegate {
    / *... * /
}
Copy the code

It’s complex and mechanical, and it’s easy to get the dependencies wrong. The dependency injection library allows us to take advantage of DI without having to provide dependencies manually, because the library generates all the code you need for it. This is where Hilt comes in.

Hilt

Hilt is a dependency injection library developed by Google that helps you take advantage of DI best practices in your applications by handling complex dependencies and generating template code that you would otherwise have to write manually.

Hilt ensures runtime performance by helping you generate code at compile time using annotations. This is done using the Dagger capability of the JVM DI library, upon which Hilt is built.

Hilt is Jetpack’s recommended Android application DI solution, which comes with tools and supports other Jetpack libraries.

Quick start

All applications that use Hilt must include an Application class annotated by @HiltAndroidApp, which triggers code generation for Hilt at compile time. In order for Hilt to inject dependencies into an Activity, the Activity needs to use the @AndroidEntryPoint annotation.

@HiltAndroidApp
class MusicApp : Application(a)@AndroidEntryPoint
class PlayActivity : AppCompatActivity() { / *... * / }
Copy the code

When injecting a dependency, add the @Inject annotation on the variable that you want to Inject. After super.oncreate is called, all Hilt injected variables will be available.

@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {

  @Inject lateinit var player: MusicPlayer

  override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(bundle)
    player.play("YHLQMDLG")
  }
}
Copy the code

In this case, we inject MusicPlayer into PlayActivity, but how does Hilt know how to provide instances of the MusicPlayer type? Extra work is needed! We also need to tell Hilt what to do, using annotations of course!

Add the @inject annotation to the constructor of the class to tell Hilt how to create instances of the class.

class MusicPlayer @Inject constructor() {
  fun play(id: String){... }}Copy the code

That’s all you need to inject a dependency into your Activity! Very simple! We’ll start with a simple example, because MusicPlayer doesn’t rely on any other type. But if we pass other dependencies as parameters, Hilt will handle and satisfy those dependencies when it provides an instance of MusicPlayer.

Actually, this is a very simple and elementary example. But if you had to do what we said manually, how would you do it?

Manual implementation

When performing DI manually, you need a dependency container that provides instances of types and manages the life cycle of those instances. That, in a nutshell, is what Hilt does behind the scenes.

When we add the @AndroidEntryPoint annotation to our Activity, Hilt automatically creates a dependency container that we manage and associate with our PlayActivity. Here we implement the PlayActivityContainer manually. By adding the @Inject annotation to MusicPlayer, you tell the container how to provide an instance of MusicPlayer.

// PlayActivity has been annotated with @androidEntryPoint
class PlayActivityContainer {

  // MusicPlayer has been annotated @inject
  fun provideMusicPlayer(a) = MusicPlayer()

}
Copy the code

In the Activity, we need to create a container instance and assign values to its dependencies on the Activity. For Hilt, adding the @AndroidEntryPoint annotation to the Activity also completes the creation of the container instance.

class PlayActivity : AppCompatActivity() {

  private lateinit var player: MusicPlayer

  Hilt creates the @AndroidEntryPoint annotation when it is added to the Activity
  private lateinit var container: PlayActivityContainer


  override fun onCreate(savedInstanceState: Bundle) {

    // @androidEntryPoint also creates and populates fields for you
    container = PlayActivityContainer()
    player = container.provideMusicPlayer()

    super.onCreate(bundle)
    player.play("YHLQMDLG")}}Copy the code

Annotations to review

So far, we have seen that when the @Inject annotation is added to the constructor of a class, it tells Hilt how to provide instances of the class. When a variable is annotated @inject and the class to which the variable belongs is annotated @AndroidEntryPoint, Hilt injects an instance of the corresponding type into the class.

The @AndroidEntryPoint annotation can be added to most Android framework classes, not just activities. It creates an instance of the dependency container for the annotated class and populates all variables annotated with @Inject.

Adding the @HILtAndroidApp annotation to the Application class, in addition to triggering Hilt generation code, creates a dependency container associated with the Application.

The Hilt module

Now that we know the basics of Hilt, let’s increase the complexity of the example. Now, in the constructor of MusicPlayer, you need a dependency MusicDatabase.

class MusicPlayer @Inject constructor(
  private val db: MusicDatabase
) {
  fun play(id: String){... }}Copy the code

Therefore, we need to tell Hilt how to provide MusicDatabase instances. When the type is an interface, or you cannot add @inject to the constructor, such as the class from a library that you cannot modify.

Suppose we use Room as a persistent repository in our application. Going back to the scenario where we implement PlayActivityContainer manually, when we provide MusicDatabase through Room, this will be an abstract class and we want to execute some code while providing the dependencies. Next, when we provide an instance of MusicPlayer, we need to call the methods that provide or satisfy MusicDatabase dependencies.

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer() = MusicPlayer(
    provideMusicDatabase()
  )
}
Copy the code

We don’t have to worry about passing dependencies in Hilt because it automatically associates all dependencies that need to be passed. However, we need to let Hilt know how to provide instances of MusicDatabase types. To do this, we use the Hilt module.

The Hilt Module is a class with the @Module annotation. In this class, we can implement functions that tell Hilt how to provide instances of the exact type. Such information Hilt knows is also known in the industry as binding.

@Module
@InstallIn(SingletonComponent::class)
object DataModule {

  @Provides
  fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
    return Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()
  }
}
Copy the code

Add the @provides annotation to this function to tell Hilt how to provide instances of the MusicDatabase type. The function body contains the block of code that Hilt needs to execute, which is exactly the same as our manual implementation.

The return type MusicDatabase tells Hilt what type this function provides. The arguments to the function tell Hilt the required dependencies for the type. In this case, ApplicationContext is already available in Hilt. This code tells Hilt how to provide an instance of the MusicDatabase type; in other words, we already have a MusicDatabase binding.

The Hilt module also needs to add an @installin annotation to indicate which dependency containers or components this information is available in. But what is a component? Let’s go into more details.

Hilt components

A component is a class generated by Hilt that provides instances of types, just like the containers we implement manually. At compile time, Hilt traverses the dependency graph and generates code to provide all types and carry their delivery dependencies.

A component is a HILT-generated class that provides instances of types

Hilt generates components (or dependency containers) for most Android framework classes. Each component association information, or binding, is passed down the component hierarchy.

△ Component hierarchy of Hilt

If MusicDatabase bindings are available in the SingletonComponent (corresponding to the Application class), then bindings are available in other components.

When you add the @AndroidEntryPoint annotation to an Android framework class, Hilt automatically generates components at compile time and creates, manages, and associates them with their corresponding classes.

The module’s @InstallIn annotation controls where these bindings are available and what other bindings they can use.

Scoped

Going back to the code that created the PlayActivityContainer manually, did you notice a problem? Each time we need a MusicDatabase dependency, we create a different instance.

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(a): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer(a) = MusicPlayer(
    provideMusicDatabase()
  )
}
Copy the code

This is not what we want, because we might want to reuse the same MusicDatabase instance throughout the application. We can share the same instance by holding a variable instead of a function.

class PlayActivityContainer {

  val musicDatabase: MusicDatabase =
    Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()

  fun provideMusicPlayer(a) = MusicPlayer(musicDatabase)
}
Copy the code

Basically we will limit the scope of the MusicDatabase type to this container because we will always provide the same instance as a dependency. How do you do this with Hilt? Well, no doubt, use another note!

In addition to the @provides annotation, we can use the @Singleton annotation to tell Hilt components that they always share the same instance of the type.

@Module
@InstallIn(SingletonComponent::class)
object DataModule {

  @Singleton  
  @Provides
  fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
    return Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()
  }
}
Copy the code

@Singleton is a scoped annotation. Each Hilt component has scoped annotations associated with it.

△ Scoped annotations for different Hilt components

If you want to limit the scope of a type to ActivityComponent, you need to use ActivityScoped annotations. These annotations can be used not only in modules but also added to classes if the constructor of the class has been annotated with @Inject.

The binding

There are two types of bindings:

  • Unscoped binding: binding without adding scoped annotations, for exampleMusicPlayerIf they are not loaded into the module, all components can use these bindings.
  • Qualified scoped binding: binding with scoped annotations added, for exampleMusicDatabase, and unscoped bindings that are loaded into a module and can be used only by the corresponding component and components below its component hierarchy.

Jetpack extensions

Hilt can be used with integration with the most popular Jetpack libraries: ViewModel, Navigation, Compose, and WorkManager.

In addition to the ViewModel, each integration requires a different library to be added to the project. For more information, see: Hilt and Jetpack integration. Do you remember the FeedViewModel code in IOsched that we saw at the beginning of this article? Would you like to see what happens with Hilt support?

@HiltViewModel
class FeedViewModel @Inject constructor(
    private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
    loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
    private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
    getTimeZoneUseCase: GetTimeZoneUseCase,
    getConferenceStateUseCase: GetConferenceStateUseCase,
    private val timeProvider: TimeProvider,
    private val analyticsHelper: AnalyticsHelper,
    private val signInViewModelDelegate: SignInViewModelDelegate,
    themedActivityDelegate: ThemedActivityDelegate,
    private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
    FeedEventListener,
    ThemedActivityDelegate by themedActivityDelegate,
    SignInViewModelDelegate by signInViewModelDelegate {
    / *... * /
}
Copy the code

To let Hilt know how to provide an instance of the ViewModel, we need to annotate @Inject on the constructor as well as the @HiltViewModel annotation on the class.

That’s it. Hilt will help you create the ViewModel provider without having to do it manually.

To learn more

Hilt builds on the Dagger of another popular dependency injection library! Dagger will be mentioned frequently in future articles! If you are using Dagger, Dagger can be used with Hilt, check out our previous article, Benefits of Moving From Dagger to Hilt. For more information about Hilt, you can refer to the following resources:

  • Dagger basics
  • Guide to migrating to Hilt
  • A memo on the difference and usage of Hilt and Dagger annotations
  • Dependency injection is implemented using Hilt
  • Codelab: Use Hilt in Android applications

That’s all for this article, and we have more MAD Skills coming up, so stay tuned for updates.

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!