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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.