Hilt Scopes & Components Explained

ARCHITECTURE › Dependency Injection

Every Hilt binding starts **unscoped**: no annotation means a brand-new instance is created on every single request. That's often exactly what you want, immutable value objects, cheap helpers. But it's also the default that surprises people who expect one shared instance without asking for it explicitly:

class Logger @Inject constructor()
// two injection sites, two separate Logger instances

Scoping annotations like @Singleton or @ViewModelScoped change that: Hilt caches one instance for the lifetime of a specific *component*, and hands that same instance back to every request within it. This lesson is about matching the right scope to the right lifetime, get it wrong and you either leak memory or silently duplicate state that should be shared.

**@Singleton is the widest scope**: it lives in SingletonComponent, created at Application#onCreate() and destroyed only when the process dies. One instance, shared for the entire app:

@Singleton
class AnalyticsClient @Inject constructor()

Every other component in Hilt descends from SingletonComponent, forming a tree: SingletonComponent down to ActivityRetainedComponent down to ActivityComponent down to FragmentComponent or ViewModelComponent and their siblings, and so on. That hierarchy isn't just organizational, it determines **visibility**: a binding installed in a component is reachable from that component and everything below it, but never from a parent or a sibling.

Two components look similar but have very different lifetimes. ActivityComponent is recreated on **every** configuration change, rotate the screen and you get a brand-new one along with a brand-new Activity. ActivityRetainedComponent is created once at the **first** onCreate and destroyed only at the **last** onDestroy, so it survives rotation:

@ActivityRetainedScoped
class FlowCoordinator @Inject constructor()
// same instance before AND after a rotation

@ActivityScoped
class ScreenAnimator @Inject constructor()
// a new instance after every rotation

This is the pair interviewers ask about most, because the naming is easy to confuse and the consequences, losing state on rotation, or holding onto it longer than intended, are very visible bugs.

**@ViewModelScoped is per-ViewModel, not global.** One instance is shared across everything injected into a *single* ViewModel, but a different ViewModel instance gets its own separate copy:

@ViewModelScoped
class CartRepository @Inject constructor()

@HiltViewModel
class CheckoutViewModel @Inject constructor(
    private val cart: CartRepository   // Checkout's own CartRepository
) : ViewModel()

@HiltViewModel
class SummaryViewModel @Inject constructor(
    private val cart: CartRepository   // a DIFFERENT CartRepository
) : ViewModel()

If you actually need one CartRepository shared *across* CheckoutViewModel and SummaryViewModel, @ViewModelScoped is the wrong tool, you need a scope from a component both ViewModels descend from, @ActivityRetainedScoped or @Singleton.

The component tree isn't just about lifetime, it also controls **who can see what**. A binding installed in a component is available there and in every child component beneath it, but never in a parent or a sibling:

@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
    @Provides
    fun provideAnalytics(@ActivityContext ctx: Context): Analytics = Analytics(ctx)
}

That Analytics binding is reachable from the Activity itself and from its Fragment and View children, because FragmentComponent and ViewComponent sit below ActivityComponent in the tree. It is **not** reachable from SingletonComponent, a Service, or a completely different Activity's subtree, those are parents or siblings, not descendants.

**The classic scoping bug: holding an Activity Context in a @Singleton.** SingletonComponent outlives every Activity, so if a singleton-scoped class stores an Activity Context, that Context, and the entire view tree it points to, never gets garbage collected after the Activity is destroyed:

// BAD: leaks every Activity that ever creates this
@Singleton
class BadAnalytics @Inject constructor(
    @ActivityContext private val ctx: Context
)

// GOOD: application Context never dies with an Activity
@Singleton
class GoodAnalytics @Inject constructor(
    @ApplicationContext private val ctx: Context
)

The fix is almost always the same: use @ApplicationContext in anything scoped wider than an Activity's own lifetime, and reserve @ActivityContext for bindings that are themselves @ActivityScoped or narrower.

In Compose, hiltViewModel() is how you retrieve a @HiltViewModel, and it's provided by ViewModelComponent, which follows the ViewModel's own lifecycle, surviving configuration changes, scoped to the current navigation destination by default:

@Composable
fun CartScreen(
    viewModel: CartViewModel = hiltViewModel()
) {
    val state by viewModel.uiState.collectAsState()
}

Anything you inject into that ViewModel with @ViewModelScoped lives and dies with it, created the first time the ViewModel is built, discarded when the ViewModel is cleared. That's a component lifetime you don't control directly (Hilt and the ViewModelStore do), which is exactly why picking the wrong scope here either duplicates state unexpectedly or holds onto it for too long.

One safeguard is worth knowing for interviews: **a scope annotation must belong to the component it's installed in**. @ActivityScoped only makes sense inside ActivityComponent; slap it on a binding installed in SingletonComponent and the build fails immediately:

// COMPILE ERROR, @ActivityScoped isn't valid in SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object BadModule {
    @ActivityScoped
    @Provides fun provideHelper(): Helper = Helper()
}

That's the whole model in one sentence: every scope annotation names a component, every component has a lifetime and a place in the tree, and a binding is visible to its component and everything beneath it. Get the component right and the lifetime, sharing behavior, and leak-safety all follow automatically.

Back to Hilt Scopes & Components