Bird
Raised Fist0
Android Kotlinmobile~15 mins

Dependency injection with Hilt in depth in Android Kotlin - Deep Dive

Choose your learning style10 modes available
Overview - Dependency injection with Hilt in depth
What is it?
Dependency Injection with Hilt is a way to provide objects that a class needs, called dependencies, without creating them inside the class. Hilt is a tool that helps Android apps get these dependencies automatically and safely. It makes your code cleaner and easier to test by managing how objects are created and shared. Instead of writing code to build dependencies yourself, Hilt does it for you.
Why it matters
Without dependency injection, classes create their own dependencies, which makes code hard to change, test, and reuse. Hilt solves this by managing dependencies centrally, so you can swap parts easily and avoid bugs. This saves time and effort, especially in big apps where many parts depend on each other. Without Hilt, apps would be more tangled and fragile, making development slower and more error-prone.
Where it fits
Before learning Hilt, you should understand basic Kotlin programming, Android app structure, and what classes and objects are. Knowing about interfaces and testing helps too. After Hilt, you can learn advanced dependency injection patterns, custom scopes, and how to combine Hilt with other Android libraries like Jetpack Compose or Retrofit.
Mental Model
Core Idea
Dependency Injection with Hilt means giving classes the objects they need from a central place, so they don’t have to build or find those objects themselves.
Think of it like...
Imagine a coffee shop where the barista doesn’t have to grow coffee beans or make milk; instead, a supplier delivers everything ready. Hilt is like that supplier, providing all ingredients so the barista can focus on making coffee.
┌───────────────┐       ┌───────────────┐
│   Client      │──────▶│  Dependency   │
│ (Needs obj)   │       │  Provider     │
└───────────────┘       └───────────────┘
         ▲                      ▲
         │                      │
         │          ┌─────────────────────────┐
         └─────────▶│  Hilt Dependency Graph  │
                    └─────────────────────────┘
Build-Up - 7 Steps
1
FoundationWhat is Dependency Injection
🤔
Concept: Dependency Injection means giving a class the objects it needs instead of making them inside the class.
In Android, classes often need other objects to work, like a database or a network client. Without injection, each class creates these objects itself, which can cause problems. Dependency Injection solves this by letting another part of the app provide these objects.
Result
Classes become simpler and easier to change because they don’t create their own dependencies.
Understanding that classes should not build their own dependencies helps keep code clean and flexible.
2
FoundationIntroduction to Hilt
🤔
Concept: Hilt is a tool that automates dependency injection in Android apps using annotations.
Hilt uses special annotations to tell the app how to provide dependencies. It builds a graph of dependencies and injects them where needed. This removes the need to write manual code for creating and passing dependencies.
Result
You can add @Inject to constructors or fields, and Hilt will provide the needed objects automatically.
Knowing that Hilt handles the creation and sharing of dependencies reduces boilerplate and errors.
3
IntermediateUsing @Inject and @Module Annotations
🤔Before reading on: do you think @Inject alone is enough to provide all dependencies in Hilt? Commit to yes or no.
Concept: Hilt uses @Inject to mark constructors for injection and @Module to define how to provide objects it cannot create by itself.
You can add @Inject to a class constructor to tell Hilt how to create it. For classes without @Inject constructors, you create a @Module with @Provides functions that return the needed objects. Hilt combines these to build the dependency graph.
Result
Hilt knows how to create and supply all dependencies, even complex ones, by combining @Inject and @Module.
Understanding the difference between @Inject and @Module helps you provide dependencies that are not simple to construct.
4
IntermediateScopes and Component Lifecycles
🤔Before reading on: do you think all dependencies in Hilt live as long as the app? Commit to yes or no.
Concept: Hilt uses scopes to control how long dependencies live, matching Android component lifecycles like activities or fragments.
You can annotate dependencies with scopes like @Singleton for app-wide single instances or @ActivityScoped for objects that live only during an activity. This helps manage memory and behavior correctly.
Result
Dependencies are created and destroyed at the right times, avoiding leaks and unnecessary work.
Knowing scopes lets you control resource use and app behavior precisely.
5
IntermediateInjecting into Android Classes
🤔
Concept: Hilt can inject dependencies directly into Android classes like Activities, Fragments, and ViewModels.
By annotating Android classes with @AndroidEntryPoint, Hilt can provide dependencies automatically. For ViewModels, you use @HiltViewModel and inject dependencies via constructor. This integrates Hilt smoothly with Android lifecycle.
Result
Your Android components receive dependencies without manual wiring, making code cleaner and safer.
Understanding how Hilt integrates with Android lifecycles simplifies app architecture.
6
AdvancedCustom Qualifiers and Multi-bindings
🤔Before reading on: can Hilt distinguish between two dependencies of the same type without extra help? Commit to yes or no.
Concept: Hilt uses qualifiers to differentiate between multiple dependencies of the same type and supports multi-bindings to provide collections of dependencies.
You create custom annotations with @Qualifier to tell Hilt which dependency to inject when types are the same. Multi-bindings let you provide sets or maps of objects, useful for plugins or strategies.
Result
You can manage complex dependency scenarios with clarity and precision.
Knowing qualifiers and multi-bindings unlocks advanced dependency management in large apps.
7
ExpertHilt’s Generated Code and Performance
🤔Before reading on: do you think Hilt uses reflection at runtime to inject dependencies? Commit to yes or no.
Concept: Hilt generates code at compile time to create and inject dependencies, avoiding runtime reflection for better performance.
Hilt’s annotation processor builds a dependency graph and generates classes that create and supply dependencies. This means injection is fast and safe, with errors caught early during compilation.
Result
Apps using Hilt have efficient startup and runtime behavior with clear error messages if dependencies are missing.
Understanding Hilt’s compile-time code generation explains why it is both fast and reliable compared to older injection methods.
Under the Hood
Hilt uses annotation processing during app compilation to scan for @Inject, @Module, and other annotations. It builds a graph of all dependencies and generates code that knows how to create and provide each object. At runtime, this generated code is called to supply dependencies without using slow reflection. Hilt also integrates with Android lifecycle components by generating specialized components and subcomponents that match lifecycles like Application, Activity, and Fragment.
Why designed this way?
Hilt was designed to simplify Dagger’s complex setup by providing standard components and lifecycle integration for Android. It avoids runtime reflection to improve performance and catches errors at compile time to help developers fix issues early. The design balances automation with explicit control, making dependency injection easier and safer for Android developers.
┌───────────────┐
│ Source Code   │
│ (@Inject,    │
│  @Module)    │
└──────┬────────┘
       │ Annotation Processor
       ▼
┌───────────────┐
│ Generated     │
│ Dependency    │
│ Graph & Code  │
└──────┬────────┘
       │ Runtime Injection
       ▼
┌───────────────┐
│ Android App   │
│ Components    │
│ (Activities,  │
│  ViewModels)  │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does Hilt create dependencies at runtime using reflection? Commit yes or no.
Common Belief:Hilt uses reflection at runtime to find and inject dependencies.
Tap to reveal reality
Reality:Hilt generates all injection code at compile time, so no reflection is used at runtime.
Why it matters:Believing in runtime reflection can lead to wrong assumptions about app performance and debugging.
Quick: Can you inject dependencies into any class without annotations? Commit yes or no.
Common Belief:You can inject dependencies into any class without marking it or its constructor.
Tap to reveal reality
Reality:Classes must have @Inject constructors or be provided by @Module functions for Hilt to inject them.
Why it matters:Missing annotations cause injection failures and runtime crashes, confusing beginners.
Quick: Are all dependencies in Hilt singletons by default? Commit yes or no.
Common Belief:All dependencies provided by Hilt live as singletons throughout the app.
Tap to reveal reality
Reality:Dependencies have no scope by default and are recreated each time unless annotated with a scope like @Singleton.
Why it matters:Assuming singleton scope can cause unexpected behavior and resource waste.
Quick: Does Hilt automatically inject dependencies into custom classes without Android lifecycle? Commit yes or no.
Common Belief:Hilt can inject dependencies into any class automatically, even if it’s not an Android component.
Tap to reveal reality
Reality:Hilt only injects Android components automatically; for other classes, you must request injection explicitly or use @Inject constructors.
Why it matters:Expecting automatic injection everywhere leads to confusion and errors in app design.
Expert Zone
1
Hilt’s component hierarchy mirrors Android lifecycles, allowing fine-grained control over dependency lifetimes beyond simple singleton or unscoped instances.
2
Using entry points, experts can access Hilt dependencies in classes not directly supported by @AndroidEntryPoint, enabling advanced integration.
3
Hilt supports assisted injection for cases where some constructor parameters are provided at runtime, a subtle but powerful feature for complex scenarios.
When NOT to use
Hilt is not ideal for pure Kotlin projects without Android or for apps requiring dynamic runtime dependency graphs. In such cases, manual injection or other DI frameworks like Koin may be better suited.
Production Patterns
In production, Hilt is used with modularized apps where each module provides its own dependencies. It is combined with ViewModel injection, Retrofit for networking, and Room for databases, ensuring clean separation and testability.
Connections
Inversion of Control (IoC)
Dependency Injection is a form of IoC where control of creating dependencies is inverted from the class to an external system.
Understanding IoC helps grasp why DI frameworks like Hilt improve code modularity and testability.
Service Locator Pattern
Both provide dependencies, but Service Locator requires classes to ask for dependencies, while DI injects them automatically.
Knowing the difference clarifies why DI leads to cleaner, more testable code than Service Locator.
Supply Chain Management
Like managing parts delivery in a factory, Hilt manages supplying objects to classes efficiently and on time.
Seeing DI as supply chain management highlights the importance of timing, scope, and reuse in software design.
Common Pitfalls
#1Forgetting to annotate Android classes with @AndroidEntryPoint.
Wrong approach:class MainActivity : AppCompatActivity() { @Inject lateinit var repo: UserRepository }
Correct approach:@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var repo: UserRepository }
Root cause:Hilt requires @AndroidEntryPoint to generate necessary code for injection in Android components.
#2Providing dependencies without scope but expecting singleton behavior.
Wrong approach:@Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides fun provideApi(): ApiService = ApiService() }
Correct approach:@Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideApi(): ApiService = ApiService() }
Root cause:Without @Singleton, Hilt creates a new instance each time, not a shared singleton.
#3Trying to inject dependencies into classes without @Inject constructor or @Module provider.
Wrong approach:class Logger { // no @Inject constructor } @Inject class UserManager(val logger: Logger) {}
Correct approach:class Logger @Inject constructor() {} @Inject class UserManager(val logger: Logger) {}
Root cause:Hilt needs a way to create dependencies, either via @Inject constructor or @Provides method.
Key Takeaways
Dependency Injection with Hilt simplifies how Android apps get the objects they need by automating creation and supply.
Hilt uses annotations to build a graph of dependencies at compile time, avoiding runtime reflection and improving performance.
Scopes in Hilt control how long dependencies live, matching Android lifecycles to manage resources efficiently.
Understanding qualifiers and modules lets you handle complex cases where multiple dependencies share the same type.
Proper use of @AndroidEntryPoint and @Inject annotations is essential to enable Hilt’s automatic injection in Android components.

Practice

(1/5)
1. What is the primary purpose of using @Module and @Provides annotations in Hilt?
easy
A. To tell Hilt how to create and supply dependencies
B. To mark an Android Activity for injection
C. To automatically generate UI components
D. To define navigation between screens

Solution

  1. Step 1: Understand the role of @Module

    @Module is used to group methods that provide dependencies to Hilt.
  2. Step 2: Understand the role of @Provides

    @Provides annotates methods inside a module to tell Hilt how to create specific dependencies.
  3. Final Answer:

    To tell Hilt how to create and supply dependencies -> Option A
  4. Quick Check:

    @Module + @Provides = supply dependencies [OK]
Hint: Remember: @Module groups, @Provides creates dependencies [OK]
Common Mistakes:
  • Confusing @Module with @AndroidEntryPoint
  • Thinking @Provides creates UI elements
  • Assuming @Module is for navigation
2. Which of the following is the correct way to inject a dependency into an Android Activity using Hilt?
easy
A. Annotate the Activity with @AndroidEntryPoint and use @Inject on the field
B. Use @Module on the Activity class
C. Call Hilt.inject(this) inside onCreate()
D. Add @Provides on the Activity constructor

Solution

  1. Step 1: Identify how to enable injection in Activity

    Hilt requires the Activity to be annotated with @AndroidEntryPoint to generate necessary code.
  2. Step 2: Inject dependencies with @Inject

    Fields or constructor parameters annotated with @Inject receive dependencies automatically.
  3. Final Answer:

    Annotate the Activity with @AndroidEntryPoint and use @Inject on the field -> Option A
  4. Quick Check:

    @AndroidEntryPoint + @Inject field = inject Activity dependency [OK]
Hint: Use @AndroidEntryPoint on Activity, then @Inject fields [OK]
Common Mistakes:
  • Trying to use @Module on Activity
  • Calling non-existent Hilt.inject() method
  • Adding @Provides on constructors
3. Given the following code snippet, what will be the output when MyViewModel is injected?
class MyRepository @Inject constructor() {
    fun getData() = "Data from repository"
}

@HiltViewModel
class MyViewModel @Inject constructor(private val repo: MyRepository) : ViewModel() {
    fun fetch() = repo.getData()
}
medium
A. "fetch() method not found"
B. NullPointerException at runtime
C. Compilation error due to missing @Provides
D. "Data from repository"

Solution

  1. Step 1: Understand constructor injection in MyRepository

    MyRepository has an @Inject constructor, so Hilt can create it automatically.
  2. Step 2: MyViewModel receives MyRepository via constructor injection

    Hilt injects MyRepository instance into MyViewModel, so calling fetch() returns repo.getData() string.
  3. Final Answer:

    "Data from repository" -> Option D
  4. Quick Check:

    Constructor injection returns data string [OK]
Hint: If constructor has @Inject, Hilt creates instance automatically [OK]
Common Mistakes:
  • Assuming @Provides is always needed
  • Expecting runtime null errors with constructor injection
  • Confusing method names in ViewModel
4. Identify the error in this Hilt module code:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient()
    }

    @Provides
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(client)
            .baseUrl("https://api.example.com")
            .build()
    }
}
medium
A. Missing @Singleton annotation on provider methods
B. Retrofit.Builder() requires a converter factory
C. OkHttpClient cannot be provided without parameters
D. NetworkModule should be a class, not an object

Solution

  1. Step 1: Review Retrofit.Builder usage

    Retrofit requires a converter factory (like GsonConverterFactory) to parse responses; missing here causes runtime error.
  2. Step 2: Check other options for correctness

    @Singleton is optional but recommended; object is valid for modules; OkHttpClient can be created without parameters.
  3. Final Answer:

    Retrofit.Builder() requires a converter factory -> Option B
  4. Quick Check:

    Retrofit needs converter factory to build properly [OK]
Hint: Retrofit always needs a converter factory to avoid errors [OK]
Common Mistakes:
  • Thinking @Singleton is mandatory for all providers
  • Confusing object vs class for modules
  • Assuming OkHttpClient needs parameters
5. You want to inject two different implementations of an interface Logger using Hilt. How can you distinguish them during injection?
hard
A. Use @Singleton on one implementation and omit on the other
B. Create two separate modules with the same provider method name
C. Use @Named qualifiers with different names on provider methods and injection points
D. Inject both implementations into a list automatically

Solution

  1. Step 1: Understand multiple implementations injection

    When multiple bindings exist for the same type, Hilt requires qualifiers to distinguish them.
  2. Step 2: Use @Named qualifiers

    Annotate provider methods and injection points with @Named("name") to specify which implementation to inject.
  3. Final Answer:

    Use @Named qualifiers with different names on provider methods and injection points -> Option C
  4. Quick Check:

    Multiple implementations need @Named qualifiers [OK]
Hint: Use @Named("tag") to differentiate implementations [OK]
Common Mistakes:
  • Assuming module separation alone distinguishes implementations
  • Using @Singleton to differentiate implementations
  • Expecting automatic list injection without setup
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard