Home Android & Kotlin Tutorials

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.

5/5 2 Ratings

Version

  • Kotlin 1.4, Android 5.0, Android Studio 4.1

Dagger is notorious for its steep learning curve. It requires the developer to know many coding principles and the code it generates isn’t intuitive. Google made an effort to simplify Dagger by trying to standardize its components, improve its performance, and making it easier to implement with different build types like test, debug and release.

The result of Google’s work is Hilt, a library that makes Dagger easier to use. Hilt uses Dagger underneath, and is, therefore, not a different dependency injection framework.

In this tutorial, you’ll learn:

  • The main design decisions behind Hilt.
  • How to configure Hilt with Gradle in your project.
  • Which standard components and scopes Hilt provides and how to use them in your app.
  • How to migrate the scoped component in your existing app to Hilt.
  • What an entry point is and why you might need one.

Note is that Hilt is Android-only, which means that you can’t use it on the server side.

Note: This tutorial assumes you’re familiar with Android development and Android Studio. If these topics are new to you, read Beginning Android Development and Kotlin for Android tutorials first.

This is the third installment of a multi-part Dagger tutorial. If you’re not familiar with Dagger, read these two tutorials before starting: Dagger 2 Tutorial For Android: Advanced and Dagger 2 Tutorial for Android: Advanced – Part 2.

Now, it’s time to dive in.

Getting Started

Download and unzip the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open the starter project using Android Studio 3.5 or greater, then build it and run it. You’ll see this:

RWNEws App

In this tutorial, you’ll work on RWNEws, a basic app that displays a list of news items. The user can select any news item to read its content.

The app is very simple, but its functionality isn’t as important as its internal construction.

Look at the project structure in Android Studio. It uses a classic Model–View–Presenter architectural pattern with definitions as in the following UML (Unified Modeling Language) class diagram:

RWNEws Architecture

This is the same app from the previous tutorials. It uses Dagger, but you’ll migrate it to Hilt. Before doing so, you need some theory, however. :]

Understanding Hilt Design Principles

In this section, you’ll learn about one of the most important design principles for Hilt.

In Android Studio, look at the code within di:

Code Structure for di package

As you see, this package contains, among other things, two different components: AppComponent and FeatureComponent.

The main difference between them is the scope. AppComponent uses a @Singleton scope, whereas FeatureComponent applies a @FeatureScope scope. As you know, every Android app contains objects with a lifecycle that can be bound to different parts of the app, like Application, Activities or Fragments.

The main change that Hilt introduces to help the developer is using predefined components for predefined scopes. This is quite similar to the pattern used in RWNEws, as you saw previously.

This approach simplifies the code and reduces the developer effort since you don’t need custom components and scopes. Different apps can now use the same naming conventions. If all components and scopes are the same, you can more easily share code between different apps.

Using Predefined Scopes and Components

You don’t need to define custom components anymore, since Hilt comes with a set of predefined components that it generates for you.

Look at the following:

Hilt component and scope hierarchy

In the diagram, each rectangle represents a predefined component together with its corresponding scope annotation. You also see arrows that represent dependency relationships between components. A component can access dependencies on any ancestor component. For instance, an object annotated with @ActivityScope can access any @Singleton scoped component.

Everything you learned about dealing with @Subcomponent and @Component dependencies is no longer relevant when working with Hilt.

Note: By default, Hilt uses a unique definition for each scoped component. For example, if your app has several Fragments using different objects, Hilt will generate a single class called FragmentComponent. Each Fragment will have its own instance using the different scoped component instances.

Alright, enough theory for the moment. It’s time to start writing some code. :]

Adding Hilt Dependencies

To use Hilt, you must install a Gradle plugin. To do this, open your project’s root build.gradle and add the following definitions in the buildscript section:

ext.hilt_android_version = "2.28-alpha"

Then add the following to the dependencies section:

classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_android_version"

The result should look like this:

buildscript {
  - - -
  ext.hilt_android_version = "2.28-alpha"
  - - -
  dependencies {
    - - -
    classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_android_version"
  }
}
Note: Version 2.28-alpha is the version available at the time of writing. When you read this tutorial, you might need to update this value.

Now, apply the plugin like this:

- - -
apply from: '../versions.gradle'
// 1
apply plugin: 'dagger.hilt.android.plugin'

android {
  - - -
}

dependencies {
  - - -
  // 2
  implementation "com.google.dagger:hilt-android:$hilt_android_version"
  kapt "com.google.dagger:hilt-android-compiler:$hilt_android_version"
  - - -
}

With these changes you:

  1. Applied the Hilt plugin to RWNEws.
  2. Added the runtime dependencies for Hilt and for the kapt compiler.

Now, remove the existing Dagger dependencies, since you don’t need them anymore:

  implementation "com.google.dagger:dagger:$dagger_version" // REMOVE
  kapt "com.google.dagger:dagger-compiler:$dagger_version" // REMOVE

Build and run the app now and you’ll get an error like this:

[Hilt]
com.raywenderlich.rwnews.di.AppModule must also be annotated with @InstallIn.
[1;31m[Hilt] Processing did not complete. 

At this stage, RWNEws is broken and needs your help to work again. Your next step will be to fix ApplicationComponent.

Note: Unfortunately, the app won’t build successfully until you finish the refactoring. Fasten your seat belt, code along and everything will be fine :]

Fixing ApplicationComponent

Hilt uses a dependency graph to represent how each component depends on the others. To build this graph, Hilt needs a single point from which to start generating the standard components you learned earlier.

In this single place, the app needs to access all the classes, with different scopes, of all the objects you need to eventually inject. You can achieve this with an Application object annotated with @HiltAndroidApp.

Next, you’ll use @HiltAndroidApp to refactor the current implementation.

Open init/InitApp.kt within the app module. At the moment, the code in this file is as follows:

// 1
class InitApp : Application() {
  // 2
  lateinit var appComponent: AppComponent

  override fun onCreate() {
    super.onCreate()
    // 3
    appComponent = DaggerAppComponent.create()
  }
  // 4
  fun appComp() = appComponent
}

Here’s a breakdown of this code:

  1. Defines a class that extends from Application.
  2. Creates an AppComponent property, which is the current component with Singleton scope.
  3. Creates the actual instance of the AppComponent implementation.
  4. Adds a utility method for accessing AppComponent from other places in the code.

Now you don’t need to create any components because Hilt does that for you. Hilt already knows that there must be a component with the Application scope, so you can replace the previous code with the following:

@HiltAndroidApp
class InitApp : Application()

With this code, you’re telling Hilt to generate the dependency graph that you’ll use to facilitate the injection later. All apps using Hilt must contain an Application annotated with @HiltAndroidApp.

Wow, the first step wasn’t bad at all! You removed a bunch of lines of code. :]

Build and run now — you’ll still have a compilation error. Before you fix that, read on to understand what happened with the code generated in this step.

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:

Generated code for the init package

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 @Scopes 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.

Note: 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.

Note: It’s worth saying that Hilt doesn’t currently support all the Android Standard Components as @AndroidEntryPoints. 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 Fragments 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:

  1. Create codeFeatureComponentProvider as an alias of Provider<FeatureComponent>.
  2. Define featureComp as a property containing the FeatureComponent you need to return as FeatureComponentProvider.
  3. Create the instance of FeatureComponent to save in featureComp.
  4. Provide the implementation of get() that the MainActivity must provide when it implements FeatureComponentProvider.

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 Fragments that will be @AndroidEntryPoint as well, and Hilt needs to know that.

Using @AndroidEntryPoint With Fragments

Hilt supports @AndroidEntryPoint for Fragments as well. Open ui/list/NewsListFragment.kt and look at its relevant code:

class NewsListFragment : Fragment(), NewsListView {

  @Inject
  lateinit var newsListPresenter: NewsListPresenter // 1
  // ...
  override fun onAttach(context: Context) {
    (context as FeatureComponentProvider).get().inject(this) // 2
    super.onAttach(context)
    Log.i(TAG, "In NewsListFragment using NewsListPresenter $newsListPresenter")
  }
  // ...
}

In this code, you:

  1. Use @Inject to get a reference to NewsListPresenter.
  2. Get the actual injection, invoking inject() on the FeatureComponent you get from the container Activity. In this case, it’s MainActivity.

The second point is what makes NewsListFragment an injection target. That’s the code Hilt writes for you when you use @AndroidEntryPoint. You just need to update NewsListFragment to the following, keeping the rest of the code unchanged:

@AndroidEntryPoint
class NewsListFragment : Fragment(), NewsListView {

  @Inject
  lateinit var newsListPresenter: NewsListPresenter // 1
  // ...
}

In this code, you:

  1. Annotated NewsListFragment with @AndroidEntryPoint, telling Hilt that this is an entry point and, therefore, a target for injection.
  2. Removed onAttach(), which used to contain the code for the injection that Hilt generates for you now.

Next, open ui/detail/NewsDetailFragment.kt and apply the same changes, removing onAttach() and adding the @AndroidEntryPoint annotation. You’ll get something like the following:

@AndroidEntryPoint
class NewsDetailFragment : Fragment(), NewsDetailView {

  @Inject
  lateinit var newsDetailPresenter: NewsDetailPresenter
  // ...
}

Cool! You were able to remove quite a bit of code because Hilt now creates the @Component for you. But there’s one last step to do.

What About @Scopes?

So far, you learned that for each predefined @Component, Hilt also provides a predefined @Scope. For the objects you’re injecting into RWNEws, you created the custom @FeatureScope scope, which you can now remove.

To do this, open presenter/impl/NewsListPresenterImpl.kt and update it to replace @FeatureScope with @FragmentScoped:

@FragmentScoped // HERE
class NewsListPresenterImpl @Inject constructor(
    private val newsRepository: NewsRepository
) : BasePresenter<NewsListModel, NewsListView>(),
    NewsListPresenter {
  // ...
}

Do the same in presenter/impl/NewsDetailPresenterImpl.kt. You’ll end up with the following code:

@FragmentScoped // HERE
class NewsDetailPresenterImpl @Inject constructor(
    private val newsRepository: NewsRepository,
    private val newsStats: @JvmSuppressWildcards(true) Set<NewsStats>
) : BasePresenter<NewsModel, NewsDetailView>(),
    NewsDetailPresenter {
  // ...
}

Now, you can finally build and run without a compilation error. You’ll get the same experience as you had with the original app:

RWNEws App

Brilliant! Those were quite a few changes, but now, finally, you’ve fully migrated your app from Dagger to Hilt. :]

IDE Support for Hilt

When you work on a big project, you might use classes with many dependencies — which means you might struggle to understand where they come from. Starting with Android Studio 4.1, the IDE supports navigating the code generated with Hilt.

To try this, open ui/list/NewsListFragment.kt. You’ll see a new icon on the left of the dependency, like this:

Android Studio icon for a provided dependency

This is the icon that Android Studio uses to represent a provided dependency. Select the icon and you’ll navigate directly to the point in the code that provides the injected object. In this example, you’ll go to FeatureModule, and more specifically, the cursor will appear next to provideNewsListPresenter.

Android Studio icon for a dependency definition

In the screenshot above, you see the second type of icon that allows you to navigate to the point in the code where the correspondent definition is injected. Select the icon next to line 18 in the example and you’ll go back to NewsListFragment.

Now, select the icon beside the name of the module — the one marked with a circle — and Android Studio will display all the locations where the module is used:

Android Studio showing module usage

You’ve now covered the fundamentals of Hilt and its APIs! :]

Where to Go From Here?

Download the final version of this project by using the Download Materials button at the top or bottom of this tutorial.

Great job completing the tutorial! You learned what Hilt is and how you can migrate your app to it from Dagger. You learned what the standard Hilt components and scopes are and how they simplify the code in your app.

What you saw here is just the beginning — Hilt can do much more for you. The next step is to learn how it can simplify testing your app. Stay tuned for coming tutorials on this topic!

To learn more about Hilt, check out the video course Dependency Injection with Hilt: Fundamentals.

If you have any comments or questions, feel free to join the discussion below.

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments