Using Dependency Injection with Hilt
1. Setting Up Hilt Dependencies
| Dependency | Version | Description |
|---|---|---|
| com.google.dagger:hilt-android | 2.51.1 |
Main Hilt library for Android |
| com.google.dagger:hilt-android-compiler | 2.51.1 |
Annotation processor for Hilt |
| androidx.hilt:hilt-navigation-compose | 1.2.0 |
Hilt integration with Jetpack Compose Navigation |
Example: Gradle Setup
plugins {
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.51.1'
annotationProcessor 'com.google.dagger:hilt-android-compiler:2.51.1'
}
Note: Ensure
android { ... } block in your app-level build.gradle includes apply plugin: 'com.google.dagger.hilt.android'2. Annotating Application Class
When using Hilt in an Android application, the Application class must be annotated with @HiltAndroidApp to enable Hilt's dependency injection capabilities.
| Annotation | Description | Usage |
|---|---|---|
| @HiltAndroidApp | Marks the Application class to enable Hilt's dependency injection. | Apply to the Application class or a subclass of it. |
Example: Annotating Application Class
@HiltAndroidApp
class MyApplication : Application() {
// Hilt will now be available for dependency injection
}
Note: The
Application class must be declared in the AndroidManifest.xml file to be recognized by the Android system.3. Injecting into Activities and Fragments
| Component | Injection Point | Annotations | Notes |
|---|---|---|---|
| Activity | onCreate() |
@AndroidEntryPoint |
Required for Hilt to inject dependencies into Activities |
| Fragment | onCreate() |
@AndroidEntryPoint |
Required for Hilt to inject dependencies into Fragments |
Example: Injecting a Repository into an Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// userRepository is now injected
val user = userRepository.getUser(1)
}
}
Example: Injecting a ViewModel into a Fragment
@AndroidEntryPoint
class UserFragment : Fragment() {
@Inject
lateinit var userViewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userViewModel is now injected
}
}
Note: Ensure that the
@AndroidEntryPoint annotation is applied to all Activities and Fragments where dependencies are injected.
4. Creating Modules
Example: Basic Hilt Module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
}
| Annotation | Description | Usage |
|---|---|---|
| @Module | Marks a class as a Hilt module | Must be applied to classes providing dependencies |
| @InstallIn | Specifies where the module is installed | Accepts component types like SingletonComponent |
| @Provides | Denotes a method that provides a dependency | Used inside modules to define dependencies |
Note: Hilt modules must be annotated with
@Module and installed in a specific component using @InstallIn.Example: Providing Different Scopes
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@Provides
@ActivityScoped
fun provideViewModel(): MyViewModel {
return MyViewModel()
}
}
Module Creation Guidelines
- Use
@Moduleto mark classes that provide dependencies - Install modules using
@InstallInwith appropriate component types - Use
@Providesto define methods that return dependencies - Apply scope annotations like
@Singletonor@ActivityScopedfor proper lifecycle management
5. Providing Dependencies
| Dependency Type | Annotation | Description | Example |
|---|---|---|---|
| Singleton | @Singleton |
Provides a single instance for the application lifecycle | @Singleton<br>class NetworkClient @Inject constructor() { } |
| Per-Activity | @ActivityScoped |
Provides a single instance per Activity lifecycle | @ActivityScoped<br>class ViewModel @Inject constructor() { } |
| Per-View | @ViewScoped |
Provides a single instance per View lifecycle | @ViewScoped<br>class FragmentViewModel @Inject constructor() { } |
| Qualifier | @Named |
Allows multiple implementations of the same type | @Named("production")<br>class ApiClient @Inject constructor() { } |
| Custom Scope | @MyCustomScope |
Defines a custom scope for dependency injection | @Scope<br>@Retention(AnnotationRetention.RUNTIME)<br>annotation class MyCustomScope |
Example: Providing a Singleton Dependency
@Singleton<br>class DatabaseManager @Inject constructor() {<br> fun saveData(data: String) { /* ... */ }<br>}
Example: Using Qualifiers for Multiple Implementations
@Named("production")<br>class ProductionApiClient @Inject constructor() : ApiClient {<br> override fun fetch(): String = "Production Data"<br>}<br><br>@Named("staging")<br>class StagingApiClient @Inject constructor() : ApiClient {<br> override fun fetch(): String = "Staging Data"<br>}
Note: Hilt automatically manages the lifecycle of dependencies based on their scope annotations.
6. Using Constructor Injection
| Component | Description | Usage |
|---|---|---|
| @Inject | Annotation to mark constructor parameters for injection | @Inject constructor(val repository: UserRepository) |
| Hilt ViewModel | Inject dependencies into ViewModels | @HiltViewModel<br>class UserViewModel @Inject constructor(<br> private val repository: UserRepository<br>) : ViewModel() |
| Activity/Fragment | Inject dependencies into Activities or Fragments | @AndroidEntryPoint<br>class MainActivity : AppCompatActivity() {<br> @Inject<br> lateinit var userManager: UserManager<br>} |
Example: Constructor Injection in a Repository
@Repository<br>class UserRepository @Inject constructor(<br> private val apiService: ApiService,<br> private val database: AppDatabase<br>) {<br> fun fetchUser(id: String) = apiService.fetchUser(id)<br>}
Example: Constructor Injection in a ViewModel
@HiltViewModel<br>class UserViewModel @Inject constructor(<br> private val repository: UserRepository,<br> private val dispatcher: CoroutineDispatcher = Dispatchers.IO<br>) : ViewModel() {<br> fun loadUser(id: String) = viewModelScope.launch(dispatcher) {<br> val user = repository.fetchUser(id)<br> // Update UI<br> }<br>}
7. Defining Scopes
| Scope | Description | Usage |
|---|---|---|
| @Singleton | Defines a scope that ensures only one instance of the class is created throughout the application's lifecycle. | @Singleton is applied to a class or module to ensure it's a singleton across the app. |
| @ActivityScoped | Ensures that a dependency is created once per Activity instance. | Used with @ActivityScoped to scope dependencies to the lifecycle of an Activity. |
| @FragmentScoped | Ensures that a dependency is created once per Fragment instance. | Applied to dependencies to scope them to the lifecycle of a Fragment. |
| @ViewModelScoped | Ensures that a dependency is created once per ViewModel instance. | Used to scope dependencies to the lifecycle of a ViewModel in Android. |
Example: Using @Singleton Scope
@Singleton
class DatabaseManager @Inject constructor(
private val database: RoomDatabase
) {
fun saveData(data: String) {
// Save data logic here
}
}
Example: Using @ActivityScoped
@ActivityScoped
class NetworkManager @Inject constructor(
private val apiService: ApiService
) {
fun fetchData(): Flow<Data> {
return apiService.fetchData()
}
}
8. Injecting ViewModel
| Component | Description | Usage |
|---|---|---|
| @HiltViewModel | Annotation to mark a ViewModel for Hilt injection | @HiltViewModel |
| HiltViewModel | Base class for Hilt-managed ViewModels | Extend HiltViewModel |
Example: Injecting ViewModel with Hilt
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
@HiltViewModel
class MyViewModel @ViewModelInject constructor(
private val repository: MyRepository
) : HiltViewModel() {
// ViewModel logic here
}
Note: The
@ViewModelInject constructor is used to inject dependencies into the ViewModel.
9. Using Qualifiers
In Hilt, qualifiers are used to distinguish between multiple implementations of the same type. This is particularly useful when you have more than one binding for a specific type, such as multiple implementations of a network interface or different database instances.
| Qualifier | Description | Usage Example |
|---|---|---|
| @Named | Simple string-based qualifier | @Named("production") |
| @Qualifier | Custom annotation for more complex qualifiers | @Qualifier with @Retention |
Example: Using @Named Qualifier
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Named("production")
fun provideProductionApi(): ApiInterface = ProductionApi()
@Provides
@Named("staging")
fun provideStagingApi(): ApiInterface = StagingApi()
}
@Singleton
class Repository @Inject constructor(
@Named("production") private val api: ApiInterface
)
Example: Using Custom @Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Production
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Production
fun provideProductionApi(): ApiInterface = ProductionApi()
}
@Singleton
class Repository @Inject constructor(
@Production private val api: ApiInterface
)
10. Testing with Hilt
Example: Providing Mock Dependencies in Tests
@HiltAndroidTest
class UserRepositoryTest {
@Inject
@Mock
lateinit var mockUserDao: UserDao
@Inject
@Mock
lateinit var mockNetworkManager: NetworkManager
@Test
fun `test fetch user data`() {
// Given
val expectedUser = User("1", "John Doe")
whenever(mockUserDao.fetchUserById("1")).thenReturn(expectedUser)
// When
val result = userRepository.fetchUser("1")
// Then
assertEquals(expectedUser, result)
}
}
| Feature | Description | Notes |
|---|---|---|
| @HiltAndroidTest | Annotation to enable Hilt in Android tests | Must be applied to test classes |
| @Mock | Creates a mock instance for injection | Used with Dagger's @Mock annotation |
| @Inject | Marker for Hilt to inject dependencies | Can be applied to fields or constructors |
Note: Hilt simplifies testing by allowing you to inject mock dependencies directly into your test classes.
Testing with Hilt Summary
- Use
@HiltAndroidTestto enable Hilt in Android tests - Use
@Mockto provide mock implementations for dependencies - Inject mocks using
@Injectin test classes - Hilt supports both unit and integration testing
11. Implementing Entry Points
| Component | Description | Usage |
|---|---|---|
| @EntryPoint | Marker annotation to define entry points in Hilt modules | Used to access dependencies from outside of Hilt's scope |
| @InstallIn | Defines the scope in which a component is installed | Used with @EntryPoint to specify where the entry point is available |
Example: Defining an Entry Point
@Module
@InstallIn(ActivityComponent::class)
@EntryPoint
interface MyEntryPoint {
fun provideSomeService(): SomeService
}
Example: Using Entry Points in an Activity
class MyActivity : AppCompatActivity() {
private val entryPoint by lazy {
EntryPointAccessors.fromActivity(this, MyEntryPoint::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val service = entryPoint.provideSomeService()
// Use the service
}
}
Note: Entry points are useful for accessing dependencies in components that are not managed by Hilt, such as Activities or Fragments.