Articles
Android Hilt Interview Questions: Scopes, Components, and Test Replacements
Explain Android Hilt scopes, component lifetimes, dependency visibility, ViewModel ownership, and test replacements with practical debugging examples.
Android Hilt interview questions are usually about ownership: which component creates a dependency, how long that component lives, and which consumers can see the binding. A scope determines sharing within a component instance. It does not make every object globally available or automatically safe to retain.
The most useful preparation is to trace a repository, a ViewModel, and an Activity through screen recreation and testing. That exposes mistakes hidden by memorizing annotation names, including leaking an Activity, replacing the wrong binding, and expecting separate ViewModels to share a ViewModel-scoped object.
In this guide, the examples are small enough to draw as a dependency graph and explain before writing code.
What does Hilt add to dependency injection?
Dependency injection supplies an object's collaborators from outside the object. Hilt builds on Dagger and provides Android-aware components and integration, reducing the amount of component setup you maintain manually.
A constructor-injected class declares its dependencies directly:
class OrdersRepository @Inject constructor(
private val api: OrdersApi
)
This tells Hilt how to construct the repository when the other required bindings are available. An interface such as OrdersApi still needs an appropriate binding or provider. A network client created by a third-party builder may require a module method rather than constructor injection.
In an interview, separate three questions: how an object is created, where its binding is installed, and whether it is scoped. Solving one does not automatically solve the other two. A perfectly valid provider method can still be unavailable from the component where a consumer is created.
What is the difference between a component and a scope?
A component is a container in the generated dependency graph with a defined lifetime and available bindings. A scope tells that component to reuse a binding's instance within its lifetime. The scope annotation must match the component where the binding is installed.
| Component | Typical lifetime | Matching scope example |
|---|---|---|
| SingletonComponent | Application process | @Singleton |
| ActivityRetainedComponent | Retained activity lifecycle across configuration changes | @ActivityRetainedScoped |
| ActivityComponent | One Activity instance | @ActivityScoped |
| ViewModelComponent | One Hilt ViewModel | @ViewModelScoped |
Do not read @Singleton as “exactly one object anywhere on the device.” It means reuse within the relevant component instance. Separate processes or separately built test graphs have different containers and instances.
An unscoped binding can produce new instances when requested by the graph. Scope an object when shared identity or its construction cost justifies it. Adding the widest scope to every binding makes ownership less clear and can retain more state than intended.
What survives an Android configuration change?
Suppose the user rotates the device. The Activity instance may be destroyed and recreated. An Activity-scoped dependency belongs to that specific Activity component, so it should not be treated as the same retained object across recreation.
The activity-retained component exists to bridge the appropriate configuration-change lifecycle. ViewModels can also survive configuration changes within their owner. This does not imply survival after process death, and it does not mean every ViewModel in the app shares one object.
Trace a concrete case: Activity A owns a ViewModel V and an Activity-scoped UI helper H1. Rotation creates Activity B and helper H2 while the retained ViewModel V remains associated with its owner. If V retained H1 or Activity A, the longer-lived object would hold a shorter-lived screen instance.
Avoid injecting an Activity reference into a retained repository or ViewModel merely because it is convenient. Pass the needed information, use an application-appropriate dependency when valid, or move screen-specific behavior to the Activity layer. Choose the lifetime from the behavior, not from which injection happens to compile.
Can one ViewModelScoped dependency be shared by multiple ViewModels?
Within a given ViewModel component, a ViewModel-scoped binding can be shared by collaborators created for that ViewModel. Different ViewModel instances have different ViewModel components and therefore do not automatically share that same scoped instance.
Imagine a checkout ViewModel whose pricing and payment helpers both use a checkout session object. @ViewModelScoped can keep those collaborators attached to one session for that ViewModel. A separate order-history ViewModel should not be assumed to receive the same checkout session.
If two ViewModels genuinely need shared state, choose an owner whose lifetime matches that state and make the sharing explicit. An activity-retained or application-level binding may fit some cases, but broader scope should not replace a clear model of session identity, account changes, and cleanup.
This is a good interview follow-up because it distinguishes component reuse from global singleton thinking. Draw the two ViewModel components and place the scoped object inside each one. The sharing rule becomes visible immediately.
How do module installation and binding visibility work?
@InstallIn places a module's bindings into specified Hilt components. Child components can access suitable bindings from their ancestors, but a parent cannot depend on a binding that exists only in a shorter-lived child.
That rule explains many compile-time errors. A singleton repository cannot require an Activity-scoped UI controller, because the application component exists independently of any particular Activity. Moving the UI controller into a singleton scope would change its lifetime and might create a leak rather than fix the design.
When a binding is missing, inspect the consumer's component, the binding's installation point, and any qualifier. When duplicate bindings appear, inspect whether two modules offer the same type and qualifier combination. The error message is evidence about the graph, not a request to add annotations at random.
Also distinguish a Kotlin import from dependency-graph visibility. Importing a type into a file only lets the compiler name it; it does not install its provider into the Hilt component that needs it.
When do you use Binds, Provides, and qualifiers?
Use @Binds for a straightforward interface-to-implementation binding when Hilt can construct the implementation. Use @Provides when construction requires a factory, builder, third-party type, or other explicit creation logic.
@Module
@InstallIn(SingletonComponent::class)
abstract class ApiBindings {
@Binds
abstract fun bindOrdersApi(impl: RealOrdersApi): OrdersApi
}
The snippet deliberately leaves scope off the binding. Whether reuse is required depends on the application's contract. Add a compatible scope only when that sharing is intended.
Qualifiers distinguish bindings of the same type. An authenticated client and a public client may both use the same client class but different credentials or interceptors. Apply the matching qualifier at the provider and injection site. A qualifier mismatch can cause a missing binding, while using the wrong valid qualifier can be a subtler behavior bug.
Do not identify dependencies only by their variable names. The graph resolves types and qualifiers, not the English meaning of a parameter named secureClient.
How do Hilt test replacements work?
For a replacement shared across tests in a source set, @TestInstallIn can replace specified production modules with test modules in the chosen components. The test binding should expose the same contract that the consumer expects.
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [ApiBindings::class]
)
object FakeApiModule {
@Provides
fun provideOrdersApi(): OrdersApi = FakeOrdersApi()
}
This is a structural example; use the project's supported Hilt setup and imports. If tests need to manipulate a single fake instance, make its ownership explicit so the test and consumer operate on the same fake rather than separately constructed instances.
For individual tests, Hilt offers other mechanisms such as module uninstallation and bound test values. Choose the narrowest mechanism that fits the desired isolation. Avoid replacing an entire graph merely to control one repository response.
Why is the real network client still used in a test?
Check the exact binding requested by the consumer. The fake may replace an unqualified interface while production code injects a qualified one. It may replace the wrong module, or the test may not be using the intended Hilt test application and runner configuration.
Next inspect initialization order. The graph must be ready at the point where the test framework or screen creates injected objects. Follow the current Hilt testing guide for rule ordering and injection rather than copying a setup from an unrelated test style.
A useful assertion is behavioral: make the fake return a unique order ID and verify it appears in the consumer's state. Also verify the fake recorded the request. Merely checking that a test module exists in the source tree proves nothing about which binding the running test received.
If a test creates the class manually with a production collaborator, Hilt replacement cannot rewrite that manually constructed object. Track the actual construction path before investigating the generated graph.
How would you debug an Activity leak?
Begin with the retained reference chain. Suppose an application-scoped analytics helper stores an Activity supplied during a screen event. The helper's lifetime exceeds the screen's lifetime, so the reference can keep the old screen alive after recreation.
Ask what the helper needs. If it only needs a stable application service, an application context may fit. If it needs to display screen UI, move that operation to a screen-owned collaborator or pass a short-lived callback with deliberate cleanup. Replacing every context with application context is not a universal solution because some operations require a themed or window-associated context.
Create a recreation test and inspect whether the old Activity remains reachable for the suspected reason. Keep the diagnosis specific: Hilt manages construction and sharing, while application fields still determine what an object retains.
A practical Hilt interview exercise
Build a small order screen with a ViewModel, repository, and API interface. Make the API replaceable by a fake. Add an Activity-scoped helper that contains no retained reference to the ViewModel's old screen, and explain why each dependency lives where it does.
Then rotate the screen and compare instance identities in a debug-only test. Predict which objects remain and which are recreated. Finally change the fake to return a failure and verify the screen's error state. This combines component lifetime, test replacement, and user-visible behavior in one exercise.
| Interview prompt | Strong answer should include |
|---|---|
| Why did this binding fail? | Consumer component, installation, type, and qualifier |
| Why is state unexpectedly shared? | Scope and actual component instance |
| Why did rotation leak a screen? | A longer-lived object retaining a shorter-lived one |
| Why did the test call the network? | Actual token, replacement module, and construction path |
Explain the graph in terms of owners and lifetimes first. The annotations become easier to remember once they describe a model you already understand.
How do you explain a dependency leak during review?
Draw the reference chain instead of merely naming the scope. For example, an application-scoped repository holds a callback, and that callback captures an activity. The repository can outlive the activity, so the callback keeps an obsolete screen reachable. Hilt creating both objects successfully does not make the ownership safe.
Ask which object should own registration and when it should remove the callback. Then test screen recreation and final destruction separately: those are different lifecycle events. Prefer passing short-lived data rather than storing an entire screen object. A leak investigation should identify the retaining reference and the intended release event before changing annotations.
Sources and Further Reading
- Android: dependency injection with Hilt
- Android: Hilt testing guide
- Hilt component hierarchy
- Hilt ViewModels
- Hilt testing
Research checked September 24, 2026. Match testing setup and lifecycle behavior to the Android and Hilt versions used by the project.