DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Hilt Dependency Injection: Complete Android DI Guide

Hilt Dependency Injection: Complete Android DI Guide

Hilt is Google's recommended dependency injection library for Android, built on top of Dagger. It simplifies DI setup by providing predefined components and container bindings.

Setup and Installation

Add Hilt to your project:

# build.gradle.kts (project)
plugins {
    id("com.google.dagger.hilt.android") version "2.48" apply false
}

# build.gradle.kts (app)
plugins {
    kotlin("kapt")
    id("com.google.dagger.hilt.android")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-compiler:2.48")
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
}
Enter fullscreen mode Exit fullscreen mode

HiltAndroidApp: Application Setup

Every Hilt project needs an Application class annotated with @HiltAndroidApp:

@HiltAndroidApp
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Hilt initializes automatically
    }
}
Enter fullscreen mode Exit fullscreen mode

This generates the DI container and must be declared in AndroidManifest.xml:

<application
    android:name=".MyApplication"
    ...>
</application>
Enter fullscreen mode Exit fullscreen mode

AndroidEntryPoint: Inject Dependencies

Mark Android classes for dependency injection:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: TaskViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // viewModel is automatically injected
    }
}

@AndroidEntryPoint
class TaskFragment : Fragment() {
    private val viewModel: TaskViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // viewModel is automatically injected
    }
}
Enter fullscreen mode Exit fullscreen mode

Supported classes: Activity, Fragment, View, Service, BroadcastReceiver.

Modules: Define Dependencies

Use @Module and @InstallIn to define how to create dependencies:

@Module
@InstallIn(SingletonComponent::class)
object DataModule {

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

    @Provides
    @Singleton
    fun provideTaskDao(database: AppDatabase): TaskDao {
        return database.taskDao()
    }

    @Provides
    @Singleton
    fun provideTaskRepository(dao: TaskDao): TaskRepository {
        return TaskRepository(dao)
    }
}
Enter fullscreen mode Exit fullscreen mode

Binds for Interfaces

Use @Binds to provide interface implementations:

interface TaskDataSource {
    suspend fun getTasks(): List<TaskEntity>
}

class LocalTaskDataSource(
    private val dao: TaskDao
) : TaskDataSource {
    override suspend fun getTasks(): List<TaskEntity> {
        return dao.getAllTasks()
    }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
    @Binds
    @Singleton
    abstract fun bindTaskDataSource(
        impl: LocalTaskDataSource
    ): TaskDataSource
}
Enter fullscreen mode Exit fullscreen mode

@Binds is more efficient than @Provides for interface bindings.

HiltViewModel and hiltViewModel()

Inject dependencies into ViewModels:

@HiltViewModel
class TaskViewModel @Inject constructor(
    private val repository: TaskRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    val tasks: StateFlow<List<TaskEntity>> = repository.allTasks
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = emptyList()
        )

    fun addTask(title: String) {
        viewModelScope.launch {
            repository.addTask(TaskEntity(title = title))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In Compose, use hiltViewModel():

@Composable
fun TaskScreen(
    viewModel: TaskViewModel = hiltViewModel()
) {
    val tasks by viewModel.tasks.collectAsState()

    LazyColumn {
        items(tasks) { task ->
            TaskRow(task)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Scopes: Component Lifetimes

Hilt provides predefined scopes for different component lifetimes:

// SingletonComponent - lives for app lifetime
@Provides
@Singleton
fun provideAppManager(context: Context): AppManager {
    return AppManager(context)
}

// ViewModelComponent - lives for ViewModel lifetime
@Provides
@ViewModelScoped
fun provideTaskInteractor(repository: TaskRepository): TaskInteractor {
    return TaskInteractor(repository)
}

// ActivityComponent - lives for Activity lifetime
@Provides
@ActivityScoped
fun provideActivityLogger(): Logger {
    return Logger("Activity")
}

// FragmentComponent - lives for Fragment lifetime
@Provides
@FragmentScoped
fun provideFragmentLogger(): Logger {
    return Logger("Fragment")
}
Enter fullscreen mode Exit fullscreen mode

Common scopes:

  • @Singleton (SingletonComponent)
  • @ViewModelScoped (ViewModelComponent)
  • @ActivityScoped (ActivityComponent)
  • @FragmentScoped (FragmentComponent)

Qualifiers: Disambiguate Same Types

When multiple implementations exist for the same type, use qualifiers:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalDataSource

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RemoteDataSource

@Module
@InstallIn(SingletonComponent::class)
object DataSourceModule {

    @Provides
    @Singleton
    @LocalDataSource
    fun provideLocalDataSource(dao: TaskDao): TaskDataSource {
        return LocalTaskDataSource(dao)
    }

    @Provides
    @Singleton
    @RemoteDataSource
    fun provideRemoteDataSource(): TaskDataSource {
        return RemoteTaskDataSource()
    }
}

@HiltViewModel
class TaskViewModel @Inject constructor(
    @LocalDataSource private val local: TaskDataSource,
    @RemoteDataSource private val remote: TaskDataSource
) : ViewModel() {
    // Both implementations injected with different qualifiers
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use @HiltAndroidApp: Every app needs exactly one
  2. @AndroidEntryPoint on UI Classes: Makes injection seamless
  3. @singleton Carefully: Only use for true app-wide singletons
  4. @ViewModelScoped for Interactors: Align lifecycle with ViewModel
  5. @Binds for Interfaces: More efficient than @Provides
  6. Qualifiers for Disambiguation: Clear and type-safe
  7. Keep Modules Organized: Group related providers
  8. Test with Testing Components: Use @UninstallModules for tests

8 Android App Templates → https://myougatheaxo.gumroad.com

Top comments (0)