If you've spent any time in the Android world, you've heard the term "Dependency Injection" (DI). It can sound intimidating, like something only senior developers with computer science degrees can understand. But the core idea is surprisingly simple, and it's one of the most powerful patterns for building clean, testable, and scalable apps.
This guide will demystify DI and show you how to implement it easily using Hilt, Google's recommended DI library for Android. By the end, you'll be injecting dependencies like a pro.
What is Dependency Injection, Really?
Let's forget the fancy terms for a second. Imagine your ProfileViewModel
needs to fetch user data. To do that, it needs a UserRepository
. The "old way" might be to create the repository inside the ViewModel.
class ProfileViewModel : ViewModel() {
// The ViewModel is responsible for creating its own dependency.
private val userRepository = UserRepository()
fun fetchUser(id: String) {
userRepository.getUser(id)
}
}
This creates two problems:
- Hard to Test: How do you test this ViewModel without making a real network call? You can't easily replace
UserRepository
with a fake one for testing. - Inflexible: What if
UserRepository
itself needs other dependencies, like a network service and a local database? The ViewModel now has to know how to construct all of those, too. It becomes a tangled mess.
Dependency Injection flips this around. Instead of a class creating its own dependencies, it receives them from an outside source. It simply says, "Hey, I need a UserRepository
to do my job," and something else provides it.
class ProfileViewModel(private val userRepository: UserRepository) : ViewModel() {
fun fetchUser(id: String) {
userRepository.getUser(id)
}
}
This is cleaner and much easier to test. But now the question is: who provides the UserRepository
? That's where Hilt comes in.
Setting Up Hilt: The 3-Step Process
Hilt is built on top of a more powerful DI library called Dagger, but it hides most of the complexity. It uses annotations to generate all the boilerplate code for you.
Step 1: Add Dependencies
In your project's root build.gradle.kts
, add the Hilt plugin. Then, in your app-level build.gradle.kts
, apply the plugin and add the Hilt dependencies.
// In root build.gradle.kts
plugins {
id("com.google.dagger.hilt.android") version "2.51.1" apply false // Use latest
}
// In app/build.gradle.kts
plugins {
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
// ...
dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-compiler:2.51.1")
}
Step 2: Annotate Your Application Class
Hilt needs to create a dependency "container" that is attached to your app's lifecycle. You do this by annotating your Application
class with @HiltAndroidApp
.
@HiltAndroidApp
class MyApplication : Application() {
// ...
}
Don't forget to register this class in your AndroidManifest.xml
: <application android:name=".MyApplication" ... >
Step 3: Annotate Your Android Components
Finally, tell Hilt which Activities, Fragments, or Services can receive dependencies by annotating them with @AndroidEntryPoint
.
@AndroidEntryPoint
class ProfileActivity : AppCompatActivity() {
// ...
}
Putting It All Together: Your First Injection
Now that Hilt is set up, let's inject our UserRepository
into the ProfileViewModel
.
1. Tell Hilt How to Create the Dependency
We need to teach Hilt how to provide an instance of UserRepository
. We do this by adding @Inject
to its constructor.
class UserRepository @Inject constructor() {
// ... logic to fetch user data
}
2. Inject it into the ViewModel
Annotate your ViewModel with @HiltViewModel
and then receive the dependency in its constructor, also using @Inject
.
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
// ...
}
3. Get the ViewModel in Your Activity/Fragment
In your @AndroidEntryPoint
annotated Activity, you can now get an instance of your ViewModel using the standard by viewModels()
delegate. Hilt will automatically provide the UserRepository
to its constructor behind the scenes.
@AndroidEntryPoint
class ProfileActivity : AppCompatActivity() {
// Hilt creates the ViewModel and injects its dependencies
private val viewModel: ProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// You can now use the viewModel
viewModel.fetchUser("123")
}
}
ProfileViewModel(UserRepository())
. You just declared what you needed, and Hilt took care of building and providing it.
This is just the beginning. Hilt can manage complex dependency graphs, handle different scopes (like singletons), and work with interfaces using modules. But by mastering these fundamentals, you've already made your app significantly cleaner, more robust, and infinitely more testable.