Basic Internal Working of Hilt Dependency Injection in Android: Key Concepts Explained

Table of Contents

Dependency Injection (DI) is a crucial concept in modern Android development, enabling better code maintainability, scalability, and testability. Hilt, built on top of Dagger, is the official dependency injection framework recommended by Google for Android. In this blog, we’ll dive deep into the internal workings of Hilt, exploring how it simplifies dependency injection, how it operates behind the scenes, and why it’s essential for building robust Android applications.

What is Dependency Injection?

Dependency Injection is a design pattern where an object’s dependencies are provided externally rather than the object creating them itself. This decouples object creation and object usage, making the code easier to test and manage.

Example without DI

Kotlin
class Engine {
    fun start() = "Engine started"
}

class Car {
    private val engine = Engine()
    fun drive() = engine.start()
}

Example with DI

Kotlin
class Car(private val engine: Engine) {
    fun drive() = engine.start()
}

Here, Engine is injected into Car, increasing flexibility and making it easier to swap or mock dependencies.

Why Hilt for Dependency Injection?

  • Simplifies the boilerplate code needed for dependency injection.
  • Manages dependency scopes automatically.
  • Integrates seamlessly with Jetpack libraries.
  • Provides compile-time validation for dependencies.

Internal Architecture of Hilt

At its core, Hilt builds upon Dagger 2, adding Android-specific integration and reducing boilerplate.

Key Components of Hilt

  1. @HiltAndroidApp: Annotates the Application class and triggers Hilt’s code generation.
  2. @AndroidEntryPoint: Used on Activities, Fragments, or Services to enable dependency injection.
  3. @Inject: Used to request dependencies in constructors or fields.
  4. @Module & @InstallIn: Define bindings and scope for dependencies.
  5. Scopes: @Singleton, @ActivityScoped, @ViewModelScoped, etc.

How Dependencies are Resolved

  • Hilt generates a component hierarchy based on annotations.
  • Dependencies are resolved from root components down to child components.
  • Each component manages its scoped dependencies.

Component Hierarchy in Hilt

Hilt creates several components internally:

  1. SingletonComponent: Application-wide dependencies.
  2. ActivityRetainedComponent: Survives configuration changes.
  3. ActivityComponent: Specific to Activity.
  4. FragmentComponent: Specific to Fragment.
  5. ViewModelComponent: Specific to ViewModel.

Flow of Dependency Resolution:

Kotlin
SingletonComponent  

ActivityRetainedComponent  

ActivityComponent  

FragmentComponent  

ViewModelComponent  

ViewComponent

Hilt’s Dual-Stage Approach

Hilt primarily provides dependencies at runtime, but it also performs compile-time validation and code generation to ensure correctness and optimize dependency injection.

Let’s see each approach in detail.

Compile-Time: Validation and Code Generation

Annotation Processing: Hilt uses annotation processors (kapt or ksp) during compile-time to scan annotations like @Inject, @HiltAndroidApp, @Module, @InstallIn, and others.

Dagger Code Generation: Hilt builds on top of Dagger, which generates code for dependency injection at compile-time. When annotations like @HiltAndroidApp, @Inject, and @Module are used, Hilt generates the required Dagger components and modules at compile-time.

Validation: 

Hilt ensures at compile-time that:

  • Dependencies have a valid scope (SingletonComponent, ActivityComponent, etc.).
  • Required dependencies are provided in modules.
  • There are no circular dependencies.

This means many potential runtime issues (like missing dependencies or incorrect scopes) are caught early at compile-time.

Note: At compile-time, Hilt generates the dependency graph and the necessary code for injecting dependencies correctly.

Run-Time: Dependency Provision and Injection

  • Once the code is compiled and ready to run, Hilt takes over at runtime to provide the actual dependency instances.
  • It uses the dependency graph generated at compile-time to resolve and instantiate dependencies.
  • Dependency injection happens dynamically at runtime using the generated Dagger components. For Example, a ViewModel with @Inject constructor gets its dependencies resolved and injected at runtime when the ViewModel is created.

Compile-Time vs Run-Time

So, while Hilt validates and generates code at compile-time, it provides and manages dependency instances at runtime.

If you’re looking for a one-liner:

“Hilt performs dependency graph generation and validation at compile-time, but the actual dependency provisioning happens at runtime.”

This dual-stage approach balances early error detection (compile-time) and flexibility in object creation (runtime).

Best Practices

  • Use @Singleton for dependencies shared across the entire application.
  • Avoid injecting too many dependencies into a single class.
  • Structure modules based on feature scope.
  • Leverage @Binds instead of @Provides when possible.

Conclusion

Hilt simplifies dependency injection in Android by reducing boilerplate and offering seamless integration with Jetpack libraries. Understanding its internal architecture, component hierarchy, and generated code can significantly improve your development process and app performance.

happy UI composeing..!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!