Skip to main content

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.

LeakCanary is the gold standard for automatic memory leak detection in Android debug builds. It instruments Activities, Fragments, ViewModels, and other objects automatically — without any code changes — and produces a clear leak trace showing the exact reference chain from a GC root to the leaked object.

Setup

Add LeakCanary as a debugImplementation dependency so it is never included in release builds:
// build.gradle.kts (app module)
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
    // No code init required; installs via ContentProvider automatically
}
No code initialization is required. LeakCanary installs itself automatically via a ContentProvider declared in its manifest — no Application.onCreate() call needed.

Watching custom objects

LeakCanary automatically watches Activities, Fragments, ViewModels, and View instances. For any other object that should become garbage after a well-defined point, register it explicitly:
// Watch custom objects beyond the built-in Activity/Fragment watchers
AppWatcher.objectWatcher.expectWeaklyReachable(myObject, "Should be GC'd after screen close")
Call this at the moment the object should no longer be reachable — for example, inside onDestroyView, after a screen is dismissed, or after a session ends. If the object has not been GC’d within five seconds, LeakCanary captures a heap dump and computes the leak trace.

CI gate with DetectLeaksAfterTestSuccess

Use DetectLeaksAfterTestSuccess in UI tests to gate your CI pipeline — any leak fails the build automatically, with no extra assertions required.
// CI gate: fail test suite on any leak
class AllScreensTest {
    @get:Rule val rule = DetectLeaksAfterTestSuccess()  // LeakCanary assertion in test
    @Test fun traverseAllScreens() { /* Espresso / UI Automator flow */ }
}
The rule hooks into the test lifecycle: after each test succeeds, it checks whether any watched objects were not GC’d. If a leak is found the test is failed with the full leak trace attached to the report.

Running LeakCanary

1

Add the dependency

Add debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14") to your app module’s build.gradle.kts and sync Gradle.
2

Run the app in debug mode

Launch a debug build on a device or emulator. LeakCanary installs itself automatically — no setup code required.
3

Navigate between screens

Open and close Activities, Fragments, and dialogs repeatedly. LeakCanary watches each object as it goes through its destruction lifecycle.
4

Check the notification for leak traces

When a leak is detected, LeakCanary posts a notification. Tap it to open the leak display screen, which shows the full reference chain from the GC root to the leaked object.

Reading the leak trace

A LeakCanary leak trace reads from top to bottom: the GC root at the top is the reason the object cannot be collected (e.g., a static field or a running thread), followed by each reference in the chain, and finally the leaked object at the bottom. Example structure:
┬───
│ GC Root: Local variable in native code

├─ android.os.MessageQueue instance
│    Leaking: NO (MessageQueue is waiting for more messages)
│    ↓ mMessages
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    ↓ callback
├─ com.example.MyActivity instance
│    Leaking: YES (Activity is destroyed and should be GC'd)
│    ↓ mContext
╰→ com.example.MyViewModel instance
     Leaking: YES (references destroyed Activity)
Key things to look for:
  • The first node marked Leaking: YES is where the problem originates.
  • The reference label on the arrow (e.g., mContext) tells you exactly which field holds the reference.
  • Work upward from the leaked object to find the retaining root — that is the code to fix.

Hilt / DI scope alignment

Mismatched DI scopes are a common source of leaks that LeakCanary will surface. A @Singleton that holds an @ActivityScoped dependency keeps the Activity alive for the entire process lifetime.
// ❌ WRONG: @Singleton dependency holds an @ActivityScoped resource
@Singleton
class GlobalCache @Inject constructor(
    private val userPrefs: UserPrefsStore  // if UserPrefsStore is @ActivityScoped, it leaks
)

// ✅ CORRECT: match lifetimes — only inject dependencies with equal or wider scope
@ActivityScoped  // lives only for one Activity
class UserDashboard @Inject constructor(
    @ActivityContext private val context: Context,  // Hilt provides activity context safely
    private val repo: DashboardRepository           // @Singleton — wider scope; fine
)
The rule is: a dependency’s scope must be equal to or wider than the scope of the class that consumes it. Injecting a narrower-scoped object into a wider-scoped container will keep the narrower object alive past its intended lifetime.

Build docs developers (and LLMs) love