Hilt - The official new DI Library for Android (Part-1)
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 LoggerLocalDataSource
in 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 @Inject
for 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, theLogDao
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 inDatabaseModule
@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 ofAppDatabase
the same asLogDao
. But different thing is thatAppDatabase
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 anActivity
(container) as a dependency, We have installed this dependency withActivityComponent::class
instead ofApplication
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
:
- Annotate
navigator
field with@Inject
to get by Hilt, - Remove the
private
visibility modifier, and - Remove the
navigator
initialization code in theonCreate
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:
- Annotate the
ButtonsFragment
with@AndroidEntryPoint
, - Remove private modifier from
logger
andnavigator
fields and annotate them with@Inject
, - Remove fields initialization code (i.e.
onAttach
andpopulateFields
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.