Hilt, The DI Library For Android – PART 2: Basic Concepts

Hilt, The DI Library For Android – PART 2: Basic Concepts

About The Series

This article is part of the following series:

  • PART 1: Getting To Know
    Here I talk little about Hilt, just how to setup Hilt for simple project and inject simple dependency into MainActivity.

  • PART 2: Basic Concepts (This Article)
    Here I talk a bit more about Hilt concepts like Hilt Bindings, Field Injection, Hilt Modules, Hilt Components, and more.

Introduction

In this article, we will learn some good concepts of Hilt that will actually make you start using it in your project.

We will learn following concepts:

So let's begin.

Inject Dependencies

Basically you have a class, that's required by an activity, a fragment or any other class in your project. You need to obtain an instance of this class from Hilt. To do so, you want to apply the following:

  1. Define AndroidEntryPoint
    Annotate your Android class (Activity, Fragment, Service, etc..) with @AndroidEntryPoint.
    Hilt provides any required instances to Android classes that have the AndroidEntryPoint annotation.

  2. Perform Field-Injection
    Annotate the required dependency with @Inject.
    This makes Hilt provide an instance of this dependency to your Android class.

  3. Define Hilt Binding
    Annotate the constructor of that dependency class with @Inject.
    This defines a Hilt Binding that makes Hilt know the necessary information to provide instances of this type as a dependency.

NOTE: If you annotate an Android class with @AndroidEntryPoint, then you also must annotate Android classes that depend on it. For example, if you annotate a fragment, then you must also annotate any activities where you use that fragment.

Let see an example in which we have MainActivity class that requires NamesAdapter as dependency.

Here we define a Hilt binding for NamesAdapter by annotating the constructor with @Inject:

public class NamesAdapter extends ... {

    @Inject
    public NamesAdapter() {
        ...
    }
    ...
}

Then we annotate MainActivity with @AndroidEntryPoint, and its dependency with @Inject:

NOTE: Hilt cannot provide dependencies for private fields.

@AndroidEntryPoint  
public class MainActivity extends ... {  

    @Inject 
    NamesAdapter mNamesAdapter;  

    ...
}

Now Hilt learns that MainActivity requires a dependency of type NamesAdapter, and knows enough of how to provide an instance of this dependency.

That's it for how to basically Inject Dependencies in Hilt.

Hilt Modules

For NamesAdapter class we actually have full control of it, we created it, thus we can define Hilt Binding using constructor injection. What about dependencies that we don't own? like a sub-classing Room class? Well, this is managed by Hilt Modules.

Hilt modules are classes we define that are used to add bindings to Hilt.

In Hilt modules, you include bindings for types that cannot be constructor injected such as interfaces or classes that are not contained in your project (ex: OkHttpClient class).

The following module, @Module tells Hilt this is a module and @InstallIn tells in which containers the bindings are available by specifying a Hilt Component:

@Module  
@InstallIn(ApplicationComponent.class)  
public class DatabaseModule  {  
    ...
}

When the ApplicationComponent is generated, all bindings defined in DatabaseModule will be available for Hilt to provide when required.

That's it for Hilt Module concept.

Hilt Components

A Hilt Component is made by of factories that know how to build stuff. It defines the lifecycle and scope of a dependency.

You can also think of a Hilt Component as a container, because you can hold onto instances of the things that factory creates.

Hilt components are encapsulated in a Component Hierarchy, which means dependencies included in upper level component will be accessible to lower level components since upper level component has a greater lifetime.

Component Hierarchy

For each Android class in which you can perform field injection, there's an associated Hilt Component. For example: ApplicationComponent is managed by Application, ActivityComponent is managed by Activity, and so on (Learn more about each Component Lifetime here).

The following module provides an instance of AppDatabase class which will be included in ApplicationComponent container:

@Module  
@InstallIn(ApplicationComponent.class)  
public class DatabaseModule  {  

    @Provides
    public AppDatabase provideDatabase(@ApplicationContext Context context) {
       Room
           .dataBuilder(context, AppDatabase.class, "app")
           .build();
    }

}

As you notice, provideDatabase() method is annotated with @Provides which means provideDatabase() method will be executed every time Hilt needs to provide an instance of AppDatabase class.

But provideDatabase() method still depends on Context, so?! Well, each Hilt container comes with a set of default bindings that can be injected as dependencies into your custom bindings, so in this example the annotation @ApplicationContext will make Hilt provide you with an instance of ApplicationContext.

That's it for Hilt Component concept.

Component Scopes

In real world you definitely need one single instance of AppDatabase database, not a new instance each time some class requires it. Well, this is managed by Component Scopes.

By default, all bindings in Hilt are not scoped. This means that each time there is a request for the binding, Hilt creates a new instance of the needed type.

What we mean by scope a binding to the component, is that Hilt will create a scoped binding once per instance of that component. For example, if you have a binding scoped to ActivityCompoent and your app has two activities, Hilt will create a scoped binding for each activity.

A scoped binding shares the same instance for all requests. So to provide a single instance of Room database, you just need to make the binding providing this instance of Room class, scoped to the component.

To scope a binding to a component, we use scope annotation that's available for each generated component. For example, for ApplicationComponent we use @Singleton and for Activity we use @ActivityScoped (You can find other annotations here).

In the following module, we define a binding for AppDatabase class that is scoped to ApplicationComponent by annotating the method with @Singleton:

@Module  
@InstallIn(ApplicationComponent.class)  
public class DatabaseModule  {  

    @Provides
    @Singleton
    public AppDatabase provideDatabase(@ApplicationContext Context context) {
       Room
           .dataBuilder(context, AppDatabase.class, "app.db")
           .build();
    }

}

Btw, what if you want a constructor injected class like NamesAdapter to be scoped too? well, just annotate the class with the proper scoping annotation as following:

@ActivityScoped
public class NamesAdapter extends ... {

    @Inject
    public NamesAdapter() {
        ...
    }
    ...
}

Before we leave this part, what if we annotated DatabaseModule with ActivityComponent instead of ApplicationComponent:

@Module  
@InstallIn(ActivityComponent.class)  
public class DatabaseModule  {  

    @Provides
    @Singleton
    public AppDatabase provideDatabase(...) {
       ...
    }

}

When Hilt encounters activity creation, it will generate a container holding all bindings of DatabaseModule, and when another different activity creation happens, it will generated different container holding different bindings of DatabaseModule. Here in this case, different activities means different generated components, and each component will hold its own scoped version of AppDatabase. So be careful with these concepts, and remember each Hilt Component is managed by the lifecycle of its corresponding Android class for which it is generated. I just wanted to harden this point with you.

That's it for Component Scopes concept.

Integration with ViewModel

Hilt includes extensions for providing classes from other Jetpack libraries. Hilt currently supports the following Jetpack components:

  • ViewModel
  • WorkManager

You must add another Hilt dependencies for Hilt to work with ViewModels:

...

dependencies {
    ...
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'

    // When using Kotlin.
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'

    // When using Java.
    annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

Now we are ready to use Hilt with ViewModel. Just inject the constructor with @ViewModelInject:

NOTE: You must also annotate the SavedStateHandle dependency with @Assisted.

public class MainViewModel extends ViewModel {

    private final MainRepository repository;
    private final SavedStateHandle savedStateHandle;

    @ViewModelInject
    public MainViewModel(
        MainRepository repository,    
        @Assisted  SavedStateHandle savedStateHandle)
    ) {
        this.repository = repository;
        this.savedStateHandle = savedStateHandle;    
    }

}

That's it. But know that this does not mean Hilt will provide you an instance of MainViewModel class, @ViewModelInject is similar to @Inject, so dependencies defined in the constructor parameters of MainViewModel will be injected by Hilt.

So in your activity/fragment that's annotated with @AndroidEntryPoint you can get the MainViewModel instance as normal using ViewModelProvider or by the by viewModels() kotlin extensions:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {

    private MainViewModel mMainViewModel;

    @Override
    protected void onCreate(Bundle savedInstaneState) {
        super.onCreate(savedInstaneState);
        mMainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
    }
}

And if you wonder if Hilt have another annotation so it can provide an instance of MainViewModel, you can check this comment from Hilt team:

So @ViewModelInject is kind of a one way thing, similar to @AndroidEntryPoint in a way. It only gets Dagger dependencies into your class. It does not add your ViewModel to the object graph as a Dagger binding. In order to access your ViewModel in a provider or somewhere else, you still need to go through the normal Android APIs of getting a view model like using a ViewModelProvider.

The reason we can't provide it into the graph is that we don't know what ViewModelStoreOwner you want to use it with.

It makes sense, right?

That's for integrating Hilt with ViewModel, and you can check the doc for integrating other parts of Jetpack with Hilt.

Hilt Notes & Warnings

We are almost done in this article, but there are just some notes and warnings to know about Hilt, I didn't want to bother you while reading, so take a quick look on them:

  • Projects that use both Hilt and data binding require Android Studio 4.0 or higher.

  • Hilt only supports activities that extend ComponentActivity, such as AppCompatActivity.

  • Hilt only supports fragments that extend androidx.Fragment.

  • Hilt does not support retained fragments.

  • Under the hood, Hilt will populate the required fields of a fragment in the onAttach() lifecycle method with instances built in the automatically generated Fragment's dependencies container.

  • Fields injected by Hilt cannot be private.

  • Bindings available in containers higher up in the hierarchy, are also available in lower levels of the hierarchy.

  • Scoping a binding to a component can be costly because the provided object stays in memory until that component is destroyed. Minimize the use of scoped bindings in your application.

More Concepts?...

There is actually more, but we should talk about them in another article. For now, let's have an idea about some:

  • Entry Point
    Hilt currently supports the following Android types: Application (by using @HiltAndroidApp), Activity, Fragment, View, Service and BroadcastReceiver, so how we inject dependencies for other classes?! Well, we need to define a custom Entry Point.

  • @Binds
    If the dependency is an interface, thus we cannot use constructor injection, so how to inject it?! To tell Hilt what implementation to use for an interface, you can use the @Bind annotation on a function inside a Hilt module.

  • Qualifiers
    Hmm, well there is a way to inject interface as dependency, but what if we have different implementations and we need to provide these implementations in the same project? Well, to tell Hilt how to provide different implementations (multiple bindings) of the same type, we use qualifiers.


So! That's all for this article. Please leave a comment if you have a question or to add value to this article.

Thank you for reading 🙏.