Hilt - The official new DI Library for Android (Part-1)

PRANAY PATEL
Simform Engineering
7 min readApr 28, 2021
Android hilt

Intro 🎤

Hilt is a new official dependency injection(DI) library for Android. It provides an organized way to use DI in your Android app by providing containers for every Android class in your project and managing their lifecycles automatically.

With the use of Hilt, our app code would be:

  • Easily refactorable
  • Reusable
  • Well testable.

The Hilt is Jetpack’s recommended library for dependency injection in Android. Hilt reduces the boilerplate of using manual DI in your project. The Hilt is built on top of the popular DI library Dagger to benefit from the compile-time correctness, runtime performance, scalability and Android Studio support that Dagger provides. So who is familiar with a dagger, may it find an easy way

In this blog guide, we would learn:

  • Set up Android project with Hilt
  • Basic of Hilt
  • Different Hilt annotation and modules

Now let’s take a look at Hilt!

Set up your Android project with Hilt 🛠

1. Adding Hilt gradle plugin hilt-android-gradle-plugin to your project's root build.gradle file:

2. Need to add Gradle plugin and dependencies in your app/build.gradle file:

By adding required dependencies as mentioned in the above steps Hilt is now ready to use. To make it easy we will learn more through the official android codelab: Using hilt in your android app

Using this codelab, we can learn how to use Hilt for the new app and refactor the existing app.

Here is the startup project which we are going to take as a reference to start integration of Hilt in-app:

This starter app contains two fragments and activity with manual injection with ServiceLocator to store logs related to buttons click and display logs using the Room database. We are going to check two branches, master is the branch which contains the initial startup project and solution is the branch which contains the complete code of the Hilt integration.

So let’s check step by step guide…

Hilt App:

💡 To use Hilt in the project, Application class is necessary with the @HiltAndroidApp annotation. Application the container is the entry point for the Hilt where it can generate all dependency’s graph.

@HiltAndroidApp
class LogApplication : Application() {
...
}

Inject dependencies into Android classes

1️⃣ @AndroidEntryPoint: Annotated on activity and fragment which creates a container related to the current activity/fragment life cycle. You can check Android classes that are supported by Hilt on this link.

@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}

💡 With @AndroidEntryPoint to the LogsFragment will create a dependency container that is attached to LogsFragment's lifecycle

2️⃣ Now we need an inject instance of different fields (like dateformatter ) in LogsFragment which we can do with @Inject annotation that will do field injection.

@AndroidEntryPoint
class LogsFragment : Fragment() {

@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}

⚠️ Access modifier shouldn’t be private while using @Inject

So now we have to remove manual initialization of logger and dateFormatter from LogFragment because now Hilt will initialize those fields for us😊 . We need to removeonAttach and populateFields methods.

Que: Now, in this case, Hilt must know how it provides instances of those dependencies( LoggerLocalDataSource and DateFormatter ) right?🤔

Ans: Add the @Inject annotation to the constructor of the class you want to be injected. To provide different instances in Hilt with @Inject with the constructor, we called its bindings.

class DateFormatter @Inject constructor() { ... }class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}

Scoping an instance to a container

Que: Let’s see ServiceLocator class again, and you can find that it returns a public LoggerLocalDataSource instance that always returns the same object when it’s called. This is what is called “scoping an instance to a container”. How can we do that in Hilt?

Ans: As we need the same instance of LoggerLocalDataSource throughout the application container. By using hilt@Singleton annotation we can achieve this.

@Singleton
class LoggerLocalDataSource @Inject constructor (private val logDao: LogDao) { ... }

Bindings (Classes) which available in higher container hierarchy, which is also available in lower container levels of the hierarchy. In the above case, we define LoggerLocalDataSourcein application hierarchy, which will be available in fragment and activity also.

Transitive dependencies:

Now we haveLoggerLocalDataSource instance as we have configured with the Hilt, but we can see it has a constructor of LogDao which we consider as a transitive dependency.

Que: As LogDao is an interface and we know that interface doesn’t have a constructor, so we can’t use @Injectfor LogDao ! So how the Hilt will provide an instance of this type?

Ans: With the use of @Provides the hilt modules to tell Hilt how to provide types that cannot be constructor injected.

Hilt Modules:

Before going to learn about @Provides we need to learn modules, which tell hilt how to provide instances of different types.

A Hilt module is a class annotated with @Module and @InstallIn. @Module tells hilt this is a module and @InstallIn tells Hilt in which containers the bindings are available by specifying a Hilt Component.

Creating a Module: Lets back to our app and create a new package called di under the hilt package and create a new file called DatabaseModule.kt inside the package:

  • Using @Module we can declare a module.
  • Corresponding components need to be in the same container, e.g LoggerLocalDataSource is scoped to the application container, the LogDao binding needs to be available in the application container.

Using @InstallIn and @Module :

@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {

}
  • Now let's create a function that uses @Provide to get @LogDao dependency in DatabaseModule
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {

@Provides
fun provideLogDao(appDatabase: AppDatabase) {
appDatabase.logDao()
}
}
  • 💡 Now again, we can see above provideLogDao the function has a transitive dependency of AppDatabase the same as LogDao . But different thing is that AppDatabase class is not generated by our app, as it’s generated by Room.
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {

...

@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"logging.db"
).build()
}
}

Que: Now new things would pop in your mind that how direct we can use @ApplicationContext?

Ans: It is one kind of default bindings that comes in each Hilt container. Here we have to access applicationContext, we need to use @ApplicationContext You can check all available default bindings here.

Now you can see LogFragment has all the necessary information to inject. Hilt needs to be aware of the MainActivity that hosts the LogFragment in order to work. We need to annotate MainActivity with @AndroidEntryPoint.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}

Using interface with @Binds:

You can find that there is a method that provides an instance ofNavigator . As AppNavigator is an interface, to inform Hilt what implementation to use for an interface, you can use the @Binds annotation on a function inside a Hilt module.

⚠️ Hilt Modules cannot contain both non-static and abstract binding methods, so you cannot place @Binds and @Provides annotations in the same class.

To perform best practices for the Hilt, new modules make code more organized. So let's create a new file called NavigationModule.kt in the di folder. Then, let's create a new abstract class called NavigationModule annotated with @Module :

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {

@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl):AppNavigator
}
  • 💡As per code, we can see that AppNavigatorImpl has an Activity (container) as a dependency, We have installed this dependency with ActivityComponent::class instead of Application container!
  • Next, we need AppNavigatorImpl and by adding @Inject with the constructor, the Hilt will be aware of it.
class AppNavigatorImpl @Inject constructor (private val activity: FragmentActivity) : AppNavigator { ... }

Hilt with Activity:

To refactor MainActivity :

  1. Annotate navigator field with @Inject to get by Hilt,
  2. Remove the private visibility modifier, and
  3. Remove the navigator initialization code in the onCreate function.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject lateinit var navigator: AppNavigator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

if (savedInstanceState == null) {
navigator.navigateTo(Screens.BUTTONS)
}
}

...
}

Now let's refactor ButtonFragment same as LogFragment . To make the class be field injected by Hilt, we have to:

  1. Annotate the ButtonsFragment with @AndroidEntryPoint,
  2. Remove private modifier from logger and navigator fields and annotate them with @Inject,
  3. Remove fields initialization code (i.e. onAttach and populateFields methods).
@AndroidEntryPoint
class ButtonsFragment : Fragment() {

@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var navigator: AppNavigator

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_buttons, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
}
}

Do you see, the ServiceLocator class no longer provides dependencies so we can remove them completely from the project.

⚔Congratulations…. Till now we have covered major and basic things you integrated Hilt in your app including modules and different annotations.⚔

So here we are end up with our part-1 for the Hilt introduction and basics.

What’s next?

In the next article, we will finish this blog and ongoing example. You’ll learn how to scope instances to containers and how we can use qualifiers. See you in part 2…

🖊 Do check out more blogs on Simform Engineering 🖊

🙏 Thanks for reading this article. Be sure to clap/recommend as much as you can and also share with your friends. It means a lot to us. Also, let’s become friends on Twitter, Github.

--

--

PRANAY PATEL
Simform Engineering

Senior Software Engineer - Android @mutualmobile | Flutter | Android | KMP | Lazy Investor