Free Dependency Injection Interview Questions & Answers

Hilt and Koin — scopes, bindings, and testable graphs.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What is dependency injection and how does it differ from a class creating its own dependencies?

    Dependency injection means a class receives the objects it needs from the outside rather than constructing them internally. Instead of a repository calling new ApiService() in its body, the ApiService is passed into its constructor by whatever wires the graph. The key benefit is inversion of control: the class no longer knows how to build its collaborators, only how to use them, which decouples it from concrete implementations, makes it testable by passing fakes, and lets a single place manage lifetimes. Newing up dependencies inside a class hardcodes the concrete type and hides the collaborator, defeating both testing and reuse.

  2. Why is a service locator considered inferior to dependency injection even though both hand you objects?

    A service locator is an object you ask for dependencies at runtime, so a class calls something like locator.get(ApiService::class) inside itself. The problem is that the dependency is now hidden: the class's constructor no longer advertises what it needs, so you cannot tell its requirements by looking at its signature, and a missing binding blows up at runtime deep inside the code. True dependency injection makes dependencies explicit in the constructor, so the compiler and the graph builder can see and validate them, and tests cannot forget to supply something because the constructor forces it. Koin sits closer to the service locator end of this spectrum, which is why its missing-definition errors surface at runtime.

  3. What is the difference between constructor injection and field injection?

    Constructor injection supplies dependencies as constructor parameters, so the object cannot exist in an invalid, half-injected state and its dependencies are immutable and obvious. Field injection instead sets annotated fields after the object has already been constructed, leaving a window where the fields are null. Constructor injection is strongly preferred because it produces final fields, is trivially testable without any framework, and makes the dependency list explicit. Field injection exists mainly for classes you do not construct yourself.

  4. Why do Android framework classes like Activity and Fragment require field injection instead of constructor injection?

    The Android system instantiates Activities and Fragments for you through reflection when it recreates them after configuration changes or process death, so you never call their constructors and therefore cannot pass dependencies in. Because you cannot control construction, Hilt injects into annotated fields inside onCreate or attach instead, which is why you see @Inject lateinit var on fields in an @AndroidEntryPoint class. This is the one place field injection is the correct choice rather than a smell. For everything you do construct yourself, such as ViewModels, repositories, and use cases, you should still use constructor injection.

  5. How does Hilt's compile-time graph validation differ from Koin's runtime resolution, and why does it matter?

    Hilt and Dagger build and validate the entire dependency graph at compile time using annotation processing, so a missing binding, a wrong scope, or a dependency cycle is a build error before the app ever runs. Koin resolves dependencies at runtime from its module DSL, so a missing definition only surfaces when the code path that needs it executes, potentially in production. The tradeoff is build speed and simplicity: Koin has almost no build overhead and pure Kotlin DSL, while Hilt catches whole classes of errors that Koin can only find through running the app or writing verification tests. For large apps the compile-time safety usually wins.

  6. What is the relationship between Hilt and Dagger?

    Hilt is a layer built on top of Dagger that provides a standard, opinionated set of components and scopes tailored to Android's lifecycle, so you no longer hand-write component interfaces and wire them to Activities and Fragments. Under the hood Hilt still generates Dagger code and uses Dagger's graph validation, so the core concepts of modules, bindings, and scopes are identical. Hilt trades some of Dagger's flexibility for drastically less boilerplate and a consistent structure across projects. You can still drop down to plain Dagger constructs when you need custom components that Hilt does not provide.

  7. What does the @AndroidEntryPoint annotation actually do?

    @AndroidEntryPoint marks an Android class such as an Activity, Fragment, Service, or BroadcastReceiver as a member-injection target, telling Hilt to generate a companion base class that performs field injection at the correct lifecycle point. For a Fragment the injection happens in onAttach, and for an Activity in super.onCreate, which is why your @Inject fields are not yet populated before those calls. It also requires the container, such as the hosting Activity of a Fragment, to itself be annotated, because Hilt threads the component down through the hierarchy. Without it, the @Inject lateinit var fields would simply never be set.

  8. What is the purpose of the @HiltAndroidApp annotation?

    @HiltAndroidApp goes on your Application subclass and is the root that triggers Hilt's code generation, creating the SingletonComponent and the base application class that holds it. It is the entry point of the entire generated dependency graph, so every other Hilt component descends from the one it establishes. Forgetting it, or forgetting to register the Application in the manifest, causes injection to fail everywhere. It effectively bootstraps the whole DI container for the app's lifetime.

  9. What is the difference between @Provides and @Binds, and why is @Binds more efficient?

    @Provides is a method whose body you write to construct and return an instance, used when you need custom logic or cannot annotate the constructor. @Binds is an abstract method that simply tells Dagger which implementation to use for an interface, taking the implementation as its single parameter and returning the interface. @Binds is more efficient because Dagger does not need to generate a factory that invokes a method body; it just aliases the interface to the already-known implementation binding, producing less generated code and no extra method call. Use @Binds whenever you are only mapping an interface to an implementation whose constructor is already injectable.

  10. Why must a @Binds method be abstract and live in an abstract class or interface module?

    @Binds has no body because it does not construct anything; it only declares a mapping from a requested type to an existing binding, so Dagger generates the wiring directly instead of calling code. An abstract method cannot have an implementation, which enforces that you are not sneaking logic in, and it must sit in an abstract class or interface module rather than an object module. A common mistake is putting @Binds in the same object module as @Provides methods, which fails to compile because object modules cannot hold abstract methods. The usual fix is a separate abstract class module, or a companion object inside the abstract module for the @Provides methods.

  11. When are you forced to use @Provides instead of @Binds?

    You must use @Provides whenever you cannot simply alias an interface to an implementation with an injectable constructor. This includes types you do not own, such as Retrofit, OkHttpClient, or a Room database, whose constructors you cannot annotate, and cases where creation requires a builder or configuration logic. It also covers providing concrete classes assembled from other dependencies or values computed at provision time. @Binds only works when the concrete type is already constructor-injectable and you just need to expose it under an interface.

  12. How do you provide a dependency for a class you do not own, such as a Retrofit or OkHttpClient instance?

    Because you cannot add @Inject to a third-party constructor, you write a @Provides method in a module that builds and returns the instance, for example a method that constructs Retrofit with a base URL and converter factory. That method can itself declare parameters for other bindings like OkHttpClient, and Hilt supplies them from the graph. You typically scope such expensive objects to SingletonComponent so a single instance is reused. This module-based provisioning is the standard bridge between Hilt's graph and libraries that know nothing about it.

  13. What does @InstallIn do and what happens if you omit it on a Hilt module?

    @InstallIn tells Hilt which component, and therefore which scope and lifetime, a module's bindings belong to, for example @InstallIn(SingletonComponent::class) makes them application-wide. Omitting @InstallIn on a Hilt module is a compile-time error, because Hilt refuses to guess where bindings should live and needs the component to build the graph correctly. Choosing the component also constrains what those bindings can depend on, since a binding can only see dependencies from its own and ancestor components. Installing something in too broad a component is a common source of leaks and incorrect sharing.

  14. Describe the main components in Hilt's hierarchy and their lifetimes.

    SingletonComponent lives for the whole application and holds app-wide singletons. ActivityRetainedComponent survives configuration changes and is created after the first Activity and destroyed on the last, which is where ViewModel-scoped state lives. ViewModelComponent is scoped to a single ViewModel's lifetime. ActivityComponent is tied to a single Activity instance and is recreated on rotation, while FragmentComponent and ViewComponent are tied to Fragments and Views respectively, and ServiceComponent to Services. Each child can access bindings from its parents, and the lifetime you pick determines how long a scoped instance survives.

  15. What is the difference between an unscoped and a scoped binding in Hilt?

    By default a binding is unscoped, meaning Hilt creates a brand-new instance every single time that type is requested, even within the same component. Adding a scope annotation like @Singleton or @ActivityScoped ties the binding to a component, so all requests within that component's lifetime receive the same cached instance. Developers often assume dependencies are singletons by default and are surprised to find multiple instances being created; if you need sharing you must explicitly scope. Scoping has a cost, so you should only scope things that genuinely need a single shared instance, such as caches or clients holding connections.

  16. What is the difference between @Singleton and @ActivityScoped, and why does the distinction matter?

    @Singleton binds an instance to the SingletonComponent so it lives for the entire application, while @ActivityScoped binds it to the ActivityComponent so a fresh instance is created for each Activity and released when the Activity is destroyed. The distinction matters because scoping to too long a component keeps objects alive far beyond their usefulness, and if that object references an Activity or View it leaks the whole context. Choosing the scope is really choosing the lifetime and the retention behavior. A misjudged scope is one of the most common DI bugs in Android.

  17. How can incorrect scoping in Hilt cause a memory leak?

    If you scope an object that holds a reference to a short-lived context, such as an Activity, View, or the Activity's Context, into a longer-lived component like SingletonComponent, that object outlives the Activity and keeps it from being garbage collected across rotations and navigations. For example, a @Singleton class that captures an Activity Context will leak every Activity that ever created it. The fix is to scope such objects no longer than the lifecycle of the context they hold, or to depend on the application Context rather than an Activity Context. Hilt's @ApplicationContext and @ActivityContext qualifiers exist precisely so you inject the right-lived Context.

  18. What is the difference between injecting @ApplicationContext and @ActivityContext, and when does each matter?

    Hilt provides two Context bindings distinguished by qualifiers: @ApplicationContext returns the long-lived application Context and @ActivityContext returns the current Activity's Context. You must use @ApplicationContext for anything stored in a singleton or long-lived object, because holding an @ActivityContext there would leak the Activity. You use @ActivityContext only for things genuinely scoped to the Activity, such as inflating views or theming that needs the Activity's resources. Picking the wrong one either leaks memory or gives you a Context that lacks the theming and window you needed.

  19. What are qualifiers and when do you need them?

    A qualifier is an annotation that disambiguates two bindings of the same type so Dagger knows which one you want, since a type alone is the key in the graph and cannot be duplicated. For example, if you provide two OkHttpClient instances, one authenticated and one not, you create @AuthenticatedClient and @UnauthenticatedClient qualifier annotations and tag both the provider and the injection site. Without qualifiers, two bindings of the same type collide and cause a duplicate-binding compile error. Qualifiers are how you keep multiple flavors of the same type unambiguous.

  20. What is the difference between @Named and a custom @Qualifier annotation?

    @Named is a built-in qualifier that disambiguates using a string value, so you write @Named("baseUrl") on both provider and consumer. A custom qualifier is your own annotation meta-annotated with @Qualifier, giving you type-safe, refactor-friendly, self-documenting names instead of stringly-typed keys. The risk with @Named is a typo in the string silently creating a different, unsatisfied binding that the compiler cannot cross-check as easily. Custom qualifiers are generally preferred for anything beyond throwaway cases because the compiler enforces the exact annotation.

  21. How do you provide and qualify coroutine dispatchers through DI, and why do it at all?

    You create qualifier annotations such as @IoDispatcher, @DefaultDispatcher, and @MainDispatcher, then write @Provides methods returning Dispatchers.IO, Dispatchers.Default, and Dispatchers.Main tagged with the matching qualifier. Injecting dispatchers instead of hardcoding them lets tests substitute a TestDispatcher so coroutine code runs deterministically and synchronously under test. Hardcoding Dispatchers.IO inside a repository makes it impossible to control timing in tests and couples the class to a specific thread pool. The qualifiers are essential because all three dispatchers are the same CoroutineDispatcher type and would otherwise collide.

  22. How do you provide an application-scoped CoroutineScope via DI, and what should it be built from?

    You provide a @Singleton CoroutineScope, typically qualified with something like @ApplicationScope, built from a SupervisorJob plus an injected dispatcher, for example CoroutineScope(SupervisorJob() + ioDispatcher). The SupervisorJob ensures one child failure does not cancel the whole scope, which is important for an app-wide scope that outlives individual operations. Injecting this scope lets long-lived work, such as writing to a cache after a request, run independently of any screen's lifecycle. You must not use viewModelScope or a lifecycle scope for work that should outlive the component, and the injected scope makes that explicit and testable.

  23. What is @HiltViewModel and how does it differ from ordinary constructor injection?

    @HiltViewModel marks a ViewModel whose constructor Hilt should populate, integrating with the ViewModel factory so that when the Activity or Fragment obtains the ViewModel via the standard viewModels delegate, Hilt supplies its dependencies. It is different from plain injection because ViewModels have their own lifecycle managed by the ViewModelProvider, so Hilt hooks into that factory rather than constructing the ViewModel directly. The dependencies live in the ViewModelComponent, created per ViewModel. You still retrieve the ViewModel through the normal Jetpack APIs, not by asking Hilt for it manually.

  24. How do you inject SavedStateHandle into a @HiltViewModel, and why can you just declare it?

    You simply add SavedStateHandle as a constructor parameter of your @HiltViewModel and Hilt provides it automatically, wired from the ViewModel's owner so it carries saved-state and navigation arguments. This works because Hilt integrates with the SavedStateViewModelFactory, which knows how to construct the handle for that ViewModel. A subtle point is that navigation arguments passed to the destination are available directly through the handle's keys, so you do not need a separate mechanism to read them. Trying to @Provides your own SavedStateHandle is a mistake, since its instance is inherently tied to the specific ViewModel owner and must come from the framework.

  25. What is assisted injection and what problem does it solve that ordinary injection cannot?

    Assisted injection handles objects that need both DI-provided dependencies and runtime parameters that are only known when the object is created, such as an id passed from a screen. You annotate the constructor with @AssistedInject, mark the runtime parameters with @Assisted, and define an @AssistedFactory interface with a create method taking those parameters. Hilt then injects the factory, and you call create at runtime with the dynamic value, letting Hilt fill in the rest. Ordinary injection cannot do this because the graph has no way to know a runtime-only value like a specific user id at build time.

  26. How does assisted injection combine with a ViewModel that needs a runtime argument?

    For a ViewModel that needs a runtime id you can use @AssistedInject with an @AssistedFactory, then supply that factory through a ViewModelProvider.Factory or the viewModels delegate's factory lambda so you pass the id when the ViewModel is created. In many modern apps, however, the cleaner path is to read the runtime argument from SavedStateHandle via navigation arguments, avoiding assisted injection entirely for that case. Assisted injection remains the right tool when the runtime value is not a navigation argument, such as an object constructed and passed in code. The key is that the DI-provided collaborators still come from the graph while only the assisted parameters are supplied by hand.

  27. What is a Provider in Dagger and when should you inject one?

    A Provider is a wrapper that defers creation, so injecting Provider<Thing> and calling get returns a fresh instance each time for unscoped bindings, or the cached instance for scoped ones, on demand rather than eagerly at construction. You inject a Provider when you need multiple instances over time, when you want to delay creation of something expensive until it is actually used, or as one way to break a dependency cycle. It differs from injecting the type directly, which resolves once at construction time. Providers give you control over how many instances you get and precisely when they are created.

  28. How do Provider and Lazy help break a circular dependency?

    A circular dependency, where A needs B and B needs A, cannot be satisfied if both must be fully constructed before the other, and Dagger reports it as a compile-time cycle error. Injecting one side as Lazy or Provider breaks the cycle because that side is not created during construction; the instance is only obtained later when get is first called, by which time the other object already exists. Lazy caches the first result, while Provider can hand back multiple instances, but both defer the actual creation past the constructor. The real fix, though, is usually to rethink the design, since a cycle often signals a missing abstraction or misplaced responsibility.

  29. Why is breaking a dependency cycle with Lazy sometimes a code smell rather than a solution?

    Wrapping a dependency in Lazy or Provider will compile away a cycle, but a cycle between two classes usually means responsibilities are tangled and each knows too much about the other. The deferred injection hides the design problem instead of resolving it, and the coupling remains at runtime. A better remedy is often to extract a shared abstraction, introduce an event or callback, or move the mutual logic into a third collaborator that both depend on. Reach for Lazy to break a genuine, unavoidable cycle, not as the default way to silence Dagger's error.

  30. What are multibindings and what do @IntoSet and @IntoMap do?

    Multibindings let several modules each contribute elements into a single collection that Dagger assembles, so you can inject a Set or Map aggregated from bindings declared in different places. @IntoSet adds each provided element to a Set, useful for a list of interceptors or initializers where order does not matter and you just want all contributions. @IntoMap adds an entry to a Map keyed by an annotation, ideal for looking up an implementation by a key such as a class or an enum. This is the standard pattern for plugin-style architectures where features register themselves without a central list.

  31. How do map keys work with @IntoMap and what is @ClassKey or a custom map key?

    When contributing to a map with @IntoMap you must also annotate the provider with a map-key annotation that specifies the key for that entry, such as the built-in @StringKey, @IntKey, or @ClassKey, or a custom key annotation you define with @MapKey. Dagger then builds a Map from those keys to the provided values, letting you inject the whole map and look up an implementation by key at runtime. A frequent use is mapping a ViewModel class to its provider, or a message type to a handler. Forgetting the map-key annotation is a compile error because Dagger cannot place the entry without a key.

  32. What is @EntryPoint and when do you need it?

    @EntryPoint lets code that Hilt does not construct reach into the dependency graph to retrieve bindings, bridging the gap for classes Hilt cannot inject directly, such as a ContentProvider, a class instantiated by another library, or a plain object. You define an interface annotated with @EntryPoint and @InstallIn for the target component, declare accessor methods, then use EntryPointAccessors to get the implementation from the appropriate component instance. This is an escape hatch for interop, not something you use for normal Activities or ViewModels. It effectively turns the graph into a controlled service locator at that one boundary.

  33. Why does a ContentProvider need @EntryPoint rather than @AndroidEntryPoint?

    A ContentProvider is created extremely early, before the Application's onCreate completes in some cases, and it is not part of Hilt's supported @AndroidEntryPoint set, so field injection through the normal mechanism is not available and its lifecycle does not fit Hilt's components. Instead you use an @EntryPoint interface installed in SingletonComponent and pull the dependencies you need via EntryPointAccessors.fromApplication. This respects the fact that the ContentProvider exists outside Hilt's managed lifecycle. It is the canonical example of when @EntryPoint is the right tool.

  34. How do you structure interface bindings across separate api and impl modules for a modularized app?

    You place the public interface and its qualifier annotations in an api module that other feature modules depend on, and keep the concrete implementation plus a Hilt module with a @Binds mapping in a separate impl module that nothing depends on directly. Consumers inject only the interface from the api module and never see the implementation, so you can swap or refactor the impl without touching callers. Hilt discovers the @Binds module at the app level where modules are assembled, wiring the interface to the impl. This is how DI enables true separation between what a feature exposes and how it is built.

  35. Why does injecting interfaces rather than concrete classes matter for modularization and build times?

    When a feature depends on an interface from an api module rather than a concrete class, its module does not need to depend on the implementation module, so implementation changes do not force recompilation of consumers and the build graph stays shallow. It also lets you replace an implementation, provide a fake in tests, or ship different implementations per build variant without changing callers. Depending on a concrete class instead pulls the whole implementation and its transitive dependencies into every consumer, coupling modules and hurting incremental build times. The interface boundary is what keeps modules independently buildable.

  36. How does Hilt make swapping in fakes for testing cleaner than manual DI?

    Hilt lets you replace entire modules in tests using @TestInstallIn or @UninstallModules combined with a test module, so a real network module is swapped for one providing a fake without changing any production code or the classes under test. Because the graph is assembled by Hilt, the substitution happens at the binding level and every consumer transparently receives the fake. With HiltAndroidTest and HiltAndroidRule the test builds a test-specific graph. This is cleaner than hand-wiring fakes everywhere because you change one binding declaration and the whole graph updates consistently.

  37. What is @TestInstallIn and how does it differ from @UninstallModules?

    @TestInstallIn is placed on a test module and names the production module it replaces, so across your whole test source set that production module is swapped globally for the test one wherever it would have been installed. @UninstallModules is placed on an individual test class to remove a production module for just that class, after which you add a replacement binding, giving per-test granularity. Use @TestInstallIn for a fake you want everywhere, and @UninstallModules when only one test needs a different binding. Mixing them up leads either to over-broad replacements or repeated boilerplate.

  38. Why can it be a mistake to scope test doubles differently from the production binding they replace?

    When you replace a binding in a test, the replacement must generally carry the same scope as the original, because Hilt validates that a binding installed in a component keeps consistent scoping, and a mismatch can cause build failures or inconsistent instances between the code under test and the assertions. If the production binding is @Singleton, the fake usually should be too, so both the class under test and the test observe the same instance. Forgetting this leads to confusing failures where the test mutates one instance while the code reads another. Keeping scope parity between real and fake bindings avoids these ghost-instance bugs.

  39. What is the difference between a Dagger subcomponent and a component dependency?

    A subcomponent is declared as a child of its parent and automatically has access to all of the parent's bindings, sharing the same object graph, which is the model Hilt uses throughout its component hierarchy. A component dependency instead treats another component as an external dependency and can only access the specific bindings that component explicitly exposes through its interface. Subcomponents are tighter and more convenient but couple the child to the parent, while component dependencies give looser coupling and let independently compiled components collaborate, which historically mattered for build isolation. Hilt hides this choice by using a fixed subcomponent hierarchy, but plain Dagger users pick deliberately.

  40. In Koin, what is the difference between single, factory, and scoped definitions?

    In Koin's module DSL, single defines a singleton created once and shared for the container's lifetime, factory creates a new instance on every resolution, and scoped ties an instance to a specific Koin scope such as an Activity's lifecycle. Choosing single versus factory is the Koin equivalent of scoped versus unscoped bindings in Hilt, and getting it wrong either shares mutable state you meant to isolate or wastes work recreating stateless objects. Scoped definitions require you to open and close the scope so instances are released at the right time. The DSL is concise but, being runtime, will not warn you if a definition is missing until resolution.

  41. How does error timing differ when a binding is missing in Koin versus Hilt?

    In Hilt a missing binding is a compile-time error, so the build fails and the app cannot ship with an unsatisfiable graph. In Koin a missing definition throws a NoBeanDefFoundException at runtime, at the moment the code tries to resolve that type, which might be a rarely hit screen that escapes testing. This is the central tradeoff between the two: Koin gains simplicity and fast builds but pushes graph errors to runtime, whereas Hilt front-loads that cost into compilation. Koin offers a checkModules or verify utility in tests to catch missing definitions earlier, but it is opt-in rather than automatic.

  42. How do you inject a ViewModel with Koin and how does it differ from @HiltViewModel?

    In Koin you declare a ViewModel with the viewModel definition in a module and retrieve it in a component using the koinViewModel or by viewModel delegate, and constructor parameters are resolved from the Koin container at runtime. Unlike @HiltViewModel, which relies on annotation processing and a generated factory validated at compile time, Koin's ViewModel wiring is resolved at runtime from the DSL, so a missing dependency for the ViewModel surfaces when the screen is opened. Koin also lets you pass runtime parameters to a ViewModel via parametersOf without a separate assisted-factory construct. The convenience comes at the cost of the compile-time guarantee Hilt provides.

  43. Why should you avoid calling get inside a Koin definition or class instead of declaring constructor parameters?

    Koin lets you resolve dependencies either by declaring constructor parameters, which Koin fills, or by calling get imperatively, and reaching for get inside a class turns Koin back into a service locator with all its hidden-dependency downsides. Declaring the dependency as a constructor parameter keeps it explicit and testable and lets Koin's verification utilities reason about it, whereas a buried get is invisible to tooling and to readers. The difference looks cosmetic but determines whether your class is honestly injectable or secretly coupled to Koin. Prefer constructor parameters and let the definition supply get calls only at the module boundary.

  44. A developer scopes a repository holding an in-memory cache as unscoped in Hilt and is confused that the cache never persists. What happened?

    Because the binding is unscoped, Hilt creates a new repository instance every time one is requested, so each injection site gets its own fresh cache and nothing is shared across the app. The developer assumed injection implies a single instance, but the default is a new instance per request. The fix is to add @Singleton, or a scope appropriate to how long the cache should live, to the binding so all consumers share one instance. This is one of the most common misunderstandings, since a cache only works if everyone holds the same object.

  45. Two Hilt bindings provide the same String type for a base URL and an API key, causing a duplicate-binding error. How do you resolve it correctly?

    The graph keys bindings by type, and two unqualified String providers collide because Dagger cannot tell which String a consumer wants. You resolve it by defining distinct qualifier annotations, such as @BaseUrl and @ApiKey, and tagging both each @Provides method and each injection site so the pair of type-plus-qualifier is unique. Simply renaming methods does not help because method names are not part of the binding key. Qualifiers are the mechanism specifically designed for multiple bindings of an otherwise identical type.

  46. Why can injecting a scoped binding into a longer-lived component fail, and what does the error tell you?

    Hilt enforces that a binding scoped to a component can only be injected within that component or its descendants, never into a parent or a longer-lived component, because the parent outlives the scope and would hold a stale or leaking instance. If you try to inject an @ActivityScoped type into a @Singleton one, Hilt reports a scope violation at compile time telling you the binding cannot be provided in that component. The lesson is that a longer-lived object cannot safely depend on a shorter-lived scoped one. You must either widen the dependency's scope appropriately or restructure so the dependency flows from longer-lived to shorter-lived, not the reverse.

  47. How does Hilt integrate with WorkManager, and why can a Worker not use plain @AndroidEntryPoint?

    A Worker is instantiated by WorkManager's own factory, not by the Android framework's normal component creation, so @AndroidEntryPoint does not apply; instead you annotate the Worker with @HiltWorker, use @AssistedInject with @Assisted for the Context and WorkerParameters, and add the Hilt WorkManager integration so Hilt provides a HiltWorkerFactory. That factory is wired into WorkManager's configuration, letting Hilt inject the Worker's other dependencies. The Context and WorkerParameters are assisted because they are runtime values WorkManager supplies. Skipping the HiltWorkerFactory setup leaves WorkManager unable to construct the injected Worker.

  48. What is the risk of eagerly initializing many @Singleton bindings, and how does Hilt actually create them?

    Hilt creates scoped instances lazily, on first request, not eagerly at application start, so simply declaring many @Singleton bindings does not slow startup by itself. The risk arises if you force eager creation, for example by injecting a large set of initializers at Application onCreate or via a startup multibinding, which then constructs everything on the main thread and delays the first frame. The guidance is to keep genuinely expensive work off the critical startup path and let lazy creation defer it until needed, or move initialization to a background scope. Understanding that scoping controls sharing and lifetime, not eagerness, prevents premature worry and misplaced optimization.

  49. How would you provide different implementations of a binding per build variant or flavor using Hilt?

    Because Hilt assembles modules at compile time, you can place variant-specific Hilt modules in the corresponding source sets, such as a debug module providing a logging or mock implementation and a release module providing the real one, both binding the same interface. Only the module in the active variant's source set is compiled into that build, so the graph resolves to the correct implementation without any runtime branching. Consumers depend only on the interface and are unaware which variant supplied it. This is cleaner than runtime if-debug checks and keeps release builds free of test scaffolding.

  50. Why is depending on an abstraction supplied through DI preferable to a class newing up its own concrete collaborators, summarized end to end?

    When a class constructs its own concrete collaborators it hardcodes their types, hides its real dependencies, controls their lifetimes locally, and becomes impossible to test without the real objects, which is exactly the coupling DI exists to remove. Injecting an abstraction instead makes dependencies explicit in the constructor, lets a central graph decide lifetimes and sharing through scopes, allows fakes to be swapped in tests, and enables modular boundaries where implementations live behind interfaces. Compile-time frameworks like Hilt and Dagger additionally validate the whole graph so missing or miscscoped wiring fails the build, while Koin trades that for runtime simplicity. The through-line is that DI turns object creation into a declarative, verifiable concern separate from object use.

Practice all Dependency Injection questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics