Jetpack Compose introduces memory leak patterns that don’t exist in the View system. The composition model — where composables recompose frequently and capture variables from their surrounding scope — creates new ways for long-lived objects like ViewModels and singletons to hold references to composition-scoped data that should have been released.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt
Use this file to discover all available pages before exploring further.
remember without a key — stale object retention
remember {} caches a value across recompositions. Without a key, the same object instance is returned even when a meaningful input changes. If that object holds open resources — threads, database connections, subscriptions — those resources remain tied to stale inputs and are never released.
Always pass every input that logically invalidates the cached object as a key to
remember. If the object holds resources, pair it with DisposableEffect using the same key to guarantee cleanup.Resources created in composition without DisposableEffect
Composable function bodies run on every recomposition. Resources opened directly in the body — database instances, media players, SDK clients — are created repeatedly and never closed when the composable leaves the tree.
Activity context captured in remember — configuration change leak
LocalContext.current in a composable may return an Activity instance. Capturing that in remember {} without a key means the same Activity reference is used across all subsequent recompositions. After a configuration change (rotation), a new Activity is created, but the remember cache still holds the old one — preventing it from being garbage collected.
Lambdas capturing composition-scoped references in long-lived objects
Lambdas defined in composables capture all variables they reference from the enclosing scope. Passing such a lambda to a singleton or SDK stores composition-scoped objects — state snapshots, binding contexts — in a location that outlives the composable. The captured references can never be collected as long as the long-lived object is alive.Composable lambda stored in ViewModel
A composable lambda captures the entire composition context — the slot table and snapshot scope. Storing it in a ViewModel prevents the composition, and everything it references, from being garbage collected. The ViewModel survives configuration changes, so the leak persists across rotations.Bare CoroutineScope() in a composable — orphaned jobs
A CoroutineScope() created without remember is re-created on every recomposition. The previous scope is abandoned without cancellation, and any coroutines it launched continue running — holding captured references — until they complete naturally.
MutableState / scope stored in application-scoped singletons
Compose snapshot state (mutableStateOf) interacts with the composition via the snapshot system. Placing snapshot state in an application-scoped singleton creates a permanent reference from the snapshot system to the composition that wrote to it. A bare CoroutineScope in a singleton is never cancelled, leaving coroutines running for the entire process lifetime.
RememberObserver work started in constructor
Objects implementing RememberObserver must start cancellable work in onRemembered, not in init {}. Work started in the constructor runs during the composition phase and may be orphaned if composition is abandoned or deferred before the slot table is applied.
LocalContext in remember without applicationContext — rotation leak
The same Activity context problem applies any time LocalContext.current is passed to a long-lived remembered object. A common instance is location service clients: passing the Activity context ties the client — and all future callbacks — to a specific Activity instance that will be destroyed on rotation.
Checklist
Jetpack Compose leaks checklist
Jetpack Compose leaks checklist
-
remember(key)correctly parameterized to invalidate when meaningful input changes - All resources opened in composables (DB, streams, players, SDK clients) use
DisposableEffectwithonDisposecleanup -
LocalContext.current.applicationContextused for long-lived remembered objects and Toast (not Activity context) - Lambdas passed to long-lived objects use
rememberUpdatedState; paired withDisposableEffectfor cleanup - No composable lambdas or composition-scoped objects stored in ViewModels
- No
CoroutineScope()created bare in a composable — userememberCoroutineScope() -
LaunchedEffectkeys correctly represent when the effect should restart - Every
DisposableEffecthas a non-emptyonDisposeblock - No
MutableStateor nakedCoroutineScopein application-scoped singletons -
RememberObserverimplementations launch work inonRemembered, notinit {} -
SharedFlowreplay buffer sized to minimum needed; large-payload flows useStateFlowor ID-only emission - Expensive in-composable computations wrapped in
remember(key)or moved to ViewModel -
LazyColumn/LazyRowitems use stablekey = { item.id } - Toast, analytics, and other side-effects placed in
LaunchedEffectwithapplicationContext
Detection and tooling checklist
Detection and tooling checklist
- LeakCanary 2.14 in debug builds with
DetectLeaksAfterTestSuccessrule in UI tests -
adb shell dumpsys meminfoActivity count checked after navigation — drops to 1 after Back - Heap dump captured via
adb shell am dumpheapor Memory Profiler after repeated navigation -
ApplicationExitInfoqueried at startup to detectREASON_LOW_MEMORYexits in production