Illustration by Claudia Sanchez

Migrating the Google I/O app to Hilt

Jose Alcérreca
Android Developers
Published in
5 min readJul 20, 2020

--

Hilt is the new library built on top of Dagger that simplifies Dependency Injection (DI) in Android apps. But, how much does it simplify it? We migrated the Google I/O app (iosched) to find out, which already used Dagger with dagger.android.

In this article I’ll go through our experience migrating this particular app. For proper and comprehensive instructions, check out the Hilt Migration Guide.

-2000, +500

We replaced 2000 lines of DI code with just 500. This is not the only success metric, but it’s promising!

How is this reduction possible? We were using dagger.android which also promised some boilerplate reduction in Android. The difference is that Hilt is much more opinionated. It already implements some concepts that work well with Android apps.

For example, you don’t need to define an AppComponent. Hilt comes with a bunch of predefined components, including the ApplicationComponent, ActivityComponent, or FragmentComponent. You can still create your own of course, Hilt is just a wrapper on top of Dagger.

Let’s dig into the details:

Android components and scoping

A problem for dependency injection in Android (actually, a general annoyance) is that components, like Activities, are created by the framework. So, in order to inject dependencies, you have to somehow do it after creation. dagger.android simplified this process by letting you call AndroidInjection.inject(this) which we did by extending DaggerAppCompatActivity or DaggerFragment. Apart from this we had a module (ActivityBindingModule) that would define which subcomponents dagger.android should create, their scope and all modules included in them using @ContributesAndroidInjector for both Activity and Fragments:

ActivityBindingModule.kt:

Additionally, each fragment had its own subcomponent, also generated with @ContributesAndroidInjector in their own module:

OnboardingModule.kt

If you look at different dagger.android projects out there, they all have similar boilerplate.

With Hilt, you just have to remove the @ContributesAndroidInjector bindings and add the @AndroidEntryPoint annotation to all Android Components (activities, fragments, views, services, and broadcast receivers) that require injection of dependencies.

Most of the modules we had, more than 20, could be removed, as they just contained @ContributesAndroidInjector (like OnboardingModule) and ViewModel bindings (more on this later) . Now you just need to annotate the fragments as entry points:

OnboardingFragment.kt:

@AndroidEntryPoint
class OnboardingFragment : Fragment() {...

For other types of bindings, we still use modules to define them and they need to be annotated with @InstallIn.

SessionViewPoolModule.kt:

@InstallIn(FragmentComponent::class)
@Module
internal class SessionViewPoolModule {

Scoping works as you’d expect with familiar predefined scopes like ActivityScoped, FragmentScoped, ServiceScoped, etc.

SessionViewPoolModule.kt:

    @FragmentScoped
@Provides
@Named("sessionViewPool")
fun providesSessionViewPool(): RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool()

Another boilerplate remover is the predefined qualifiers, like @ApplicationContext or @ActivityContext which saves you from having to create the same bindings in all apps.

    @Singleton
@Provides
fun providePreferenceStorage(
@ApplicationContext context: Context
): PreferenceStorage = SharedPreferenceStorage(context)

Android Architecture Components

Where Hilt really shines is with its integration with Architecture Components. It supports injection of ViewModels and WorkManager Workers.

Before Hilt, doing this required a deep understanding of Dagger (or good copy-paste skills, as most projects had the same setup).

First, we provided a ViewModel factory for fragments and activities to obtain ViewModels via ViewModelProviders:

With Hilt we can obtain ViewModels in fragments with a single line:

private val viewModel: AgendaViewModel by viewModels()

or, if you want to scope to the parent activity:

private val mainActivityViewModel: MainActivityViewModel by activityViewModels()

Secondly, before Hilt, using injected dependencies inside the ViewModels required a complicated multibindings setup using a ViewModelKey:

And in each module you would provide it like so:

SessionDetailModule.kt:

With Hilt, we add the @ViewModelInject annotation to the ViewModel’s constructor. That’s it. No need to define them in modules or add them to a magical map.

class SessionDetailViewModel @ViewModelInject constructor(...) { … }

Note that this is part of Hilt and Jetpack integrations and you need to define extra dependencies to use them.

Testing

Unit testing

Unit testing doesn’t change. Your architecture should allow for testing your classes independently of how you create your object graph.

Instrumented tests — test runner setup

Using Instrumented tests with Hilt changes a bit with respect to Dagger. It all starts with a custom test runner that lets you define a different test application:

Before:

After:

Instead of returning a test application with a different Dagger graph for tests in the newApplication method, we need to return CustomTestRunner_Application. The actual test application is defined in the @CustomTestApplication annotation. You only need this class if there’s some important initialization to do. In our case it was AndroidThreeTen and we added Timber as well.

Before, we had to tell Dagger which AndroidInjector to use and we could extend the main application:

With Hilt, the MainTestApplication can’t extend your existing application because it’s already annotated with @HiltAndroidApp. We need to create a new Application and define the important initialization steps here:

That’s it for testing. This Application will replace the MainApplication when running instrumented tests.

Instrumented tests — test classes

The actual test classes vary as well. Since we don’t have an AppComponent (or TestAppComponent) anymore, all modules and dependencies installed in the predefined ApplicationComponent are going to be available at test time. Oftentimes, however, you want to replace some of those modules.

For example, in iosched we replace the CoroutinesModule for a TestCoroutinesModule that flattens execution so it’s synchronous, repeatable and consistent. This TestCoroutinesModule is simply added to the androidTest directory and it’s installed in the ApplicationComponent normally:

However, at this time we would have “duplicated bindings” errors because two modules (CoroutinesModule and TestCoroutinesModule) can provide the same dependencies. To solve this, we simply uninstall the production module in the test class using the @UninstallModules annotation.

@HiltAndroidTest
@UninstallModules(CoroutinesModule::class)
@RunWith(AndroidJUnit4::class)
class AgendaTest {...

Also, we need to add the @HiltAndroidTest annotation and the @HiltAndroidRule JUnit rule to the test class. There’ s one thing you have to take into account though:

HiltAndroidRule order

One important thing to note is that the HiltAndroidRule must be processed before the activity is launched. It’s probably a good idea to run it before any other rule.

Before JUnit 4.13 you can use RuleChain to define the order but personally I’ve never liked the outer/inner rule concept. In 4.13 a simple order parameter was added to the @Rule annotations, making them much more readable:

Remember that if you don’t define the order you’ll be introducing a race condition and a subtle bug that will show up, probably, at the worst time.

Should you use Hilt?

Like everything released in Jetpack, Hilt is a way for you to code Android applications faster, but it’s still optional. If you are confident in your Dagger skills, there’s probably no reason for you to migrate. However, if you work with a diverse team where not everyone eats multibindings for breakfast, you should consider simplifying your codebase using Hilt. Build times are similar and the dex method count is similar to dagger.android’s.

Also, if you don’t use a DI framework, now is the time. I recommend starting with the codelab, which doesn’t assume any Dagger knowledge!

--

--

Jose Alcérreca
Android Developers

Developer Relations Engineer @ Google, working on Android