name: android-debugging description: > Use when debugging Android or KMP issues — Android-specific techniques covering Logcat, ADB, ANR traces, R8 stack trace decoding, memory leaks, Gradle build failures, and Compose recomposition bugs, on a root-cause-first foundation.
Android Debugging
Overview
Android-specific evidence-gathering and investigation techniques on a root-cause-first foundation.
Root-cause-first foundation:
- No fix before the root cause is found. Investigate first — reproduce, gather evidence (observed values, not assumed), trace the cause. Patching a symptom you don't understand creates two bugs.
- Three failed guesses ⇒ stop and question the architecture rather than guessing again.
The Android-specific tools below serve that investigation. Optional: if you run a dedicated debugging-discipline skill (e.g. superpowers:systematic-debugging or ace:systematic-debugging), this layers on top of it — but it requires none.
Evidence-Gathering by Problem Type
Crashes & Exceptions
# Stream crash logs filtered by app package
adb logcat --pid=$(adb shell pidof -s com.example.app)
# Save full logcat to file for analysis
adb logcat -d > crash_log.txt
# Filter by tag
adb logcat -s "YourTag:E"
Key logcat log levels: V (verbose) D (debug) I (info) W (warn) E (error) F (fatal)
Read the full stack trace — the root cause is usually at the bottom of the Caused by: chain, not the top-level exception.
ANR (Application Not Responding)
ANRs mean the main thread was blocked. Evidence:
# Pull ANR trace from device
adb pull /data/anr/traces.txt ./anr_traces.txt
# Or stream while reproducing
adb logcat -s "ActivityManager:E" | grep -A 30 "ANR in"
Look for: main thread in state MONITOR (waiting for a lock) or blocking I/O on main. Trace backward to find what holds the lock.
Common causes: Database/network call on main thread, runBlocking on main thread, deadlock between coroutine scopes.
Memory Leaks
Add LeakCanary to debugImplementation. It surfaces leak traces automatically in a notification.
Read the leak trace top-to-bottom: the first bold line is the leaking object, the path shows what's holding the reference. Fix by clearing the reference in the appropriate lifecycle callback.
# Dump heap manually for Android Profiler analysis
adb shell am dumpheap com.example.app /data/local/tmp/heap.hprof
adb pull /data/local/tmp/heap.hprof ./heap.hprof
Performance Trace Investigation (Perfetto)
For bottleneck investigation across CPU, graphics, I/O, IPC, memory, or power — beyond what Logcat and ANR traces show — capture a Perfetto trace and query it with SQL.
Measure before you fix. For performance regressions, logs usually mislead — capture a baseline measurement before changing anything, then bisect against it. A trace tells you where time goes; only a comparison against a known-good baseline tells you what actually regressed.
# Capture a trace (system-level, all categories)
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace.pftrace \
<<'EOF'
buffers { size_kb: 65536 }
data_sources { config { name: "linux.ftrace" } }
data_sources { config { name: "android.surfaceflinger.frame" } }
duration_ms: 10000
EOF
adb pull /data/misc/perfetto-traces/trace.pftrace ./
Then open the trace at https://ui.perfetto.dev and run SQL against it (SELECT name, dur FROM slice WHERE dur > 16e6 for frames slower than 16ms, etc.).
For an agent-driven workflow — translating an investigation intent (jank, slow startup, battery drain) into the right Perfetto SQL and iterating across the trace — see Google's perfetto-sql and perfetto-trace-analysis skills (android skills list to check for a local install; android skills add perfetto-sql perfetto-trace-analysis otherwise). They provide Domain Hints (CPU/Graphics/I/O/IPC/Memory/Power), a mandatory scratchpad chain-of-evidence pattern, and GLOB-over-LIKE query rules.
R8 / ProGuard — Obfuscated Stack Traces
Release crash stack traces are obfuscated. Decode them with the mapping file generated at build time.
# retrace a crash (AGP 7+)
./gradlew :app:retrace --stacktrace-file crash.txt
# Or use the retrace CLI directly
java -jar retrace.jar mapping.txt crash.txt
Mapping files are in app/build/outputs/mapping/<variant>/mapping.txt. Always archive them alongside release builds.
If a class is unexpectedly removed or renamed, add a -keep rule in proguard-rules.pro and verify with:
./gradlew :app:assembleRelease
# Then inspect: app/build/outputs/mapping/release/usage.txt (removed) and seeds.txt (kept)
For the inverse problem — reading obfuscated third-party code or decoding a stack trace from a library where the mapping file isn't yours — retrace doesn't apply. Use jadx --deobf (consistent renames across the decompiled output) or jadx --deobf-map (when the SDK ships a mapping). The android-reverse-engineering plugin covers the full workflow including the anchor-via-strings strategy for navigating obfuscated code by string literals and framework class names that survive obfuscation (check if it's already installed locally first — it ships as android-reverse-engineering:* skills).
Gradle Build Failures
Read the error from the bottom up — Gradle wraps errors in multiple layers.
Common patterns:
| Error | Investigation |
|---|---|
Manifest merger failed |
Check app/build/intermediates/merged_manifests/ for the merged output; look for conflicting android: attributes |
Duplicate class |
Run ./gradlew dependencies and look for the same class in multiple transitive deps; use exclude or force a version |
Could not resolve |
Check repository declarations, VPN/proxy, dependency version exists |
D8/R8: Type not present |
Missing keep rule or desugaring issue; check minSdk vs API used |
KSP / KAPT error |
Look for the processor's own error above the Gradle wrapper message |
# Full dependency tree for a configuration
./gradlew :app:dependencies --configuration releaseRuntimeClasspath
# Run with stacktrace for deeper Gradle errors
./gradlew assembleDebug --stacktrace --info 2>&1 | grep -A 20 "FAILED"
Runtime UI Inspection
When a bug is visual (wrong element state, missing content, overlap), dump the layout tree directly instead of reasoning from a screenshot:
# Full layout tree as JSON — search by class/text/bounds instead of parsing an image
android layout --pretty
# Only elements that changed since last call — useful for animations or transient state
android layout --diff --pretty
# Target a specific device, write to file
android layout --device=emulator-5554 -o layout.json
Prefer android layout over adb screencap whenever the question is "what is the UI state?" rather than "what does it look like?". The JSON tree is grep-able and survives --diff state across invocations.
Compose Recomposition Bugs
For deeper Compose performance analysis (stability, recomposition skipping, baseline profiles), see android-skills:compose → references/performance.md.
Wrong state or unexpected re-renders:
- Layout Inspector (Android Studio) → enable "Show recomposition counts" to identify hot paths. For headless/CLI workflows,
android layout --diffgives a JSON tree of what changed between frames. - Add
SideEffect { Log.d("Recompose", "MyComposable recomposed") }temporarily to confirm - Check that
Stateobjects are not created inside the composition (useremember) - Verify
equals()on state data classes — a new object with same values still triggers recomposition ifequalsis not implemented
Note: Since Compose compiler 2.0+ (Kotlin 2.0+), strong skipping mode is enabled by default and the compiler automatically memoizes lambdas that capture stable references. Manual remember {{ }} wrapping is no longer necessary in most cases. If you see excessive recomposition from lambdas, check whether the captured references are unstable (mutable collections, non-data classes) rather than wrapping in remember.
ADB Quick Reference
# List connected devices
adb devices
# Install APK
adb install -r app-debug.apk
# Launch activity
adb shell am start -n com.example.app/.MainActivity
# Clear app data
adb shell pm clear com.example.app
# Take screenshot (for visual diffing; for UI state bugs, prefer `android layout` — see Runtime UI Inspection)
adb exec-out screencap -p > screen.png
# View running processes
adb shell ps | grep com.example
# Check app's SharedPreferences / databases
adb shell run-as com.example.app ls /data/data/com.example.app/
Multi-Component Evidence Template
For issues spanning multiple layers (e.g. Repository → ViewModel → UI):
// Temporarily instrument each boundary with a UNIQUE run-specific tag
// (pick a fresh suffix per session, e.g. DEBUG-a4f2) so the SAME tag both
// filters logcat at runtime and greps cleanly at teardown.
class UserRepository(...) {
suspend fun fetchUser(id: String): User {
Log.d("DEBUG-a4f2", "Repository: fetching user $id")
val result = api.getUser(id)
Log.d("DEBUG-a4f2", "Repository: received ${result}")
return result
}
}
Run once to identify which layer produces the bad value. Filter the run at runtime with adb logcat -s "DEBUG-a4f2", then tear the instrumentation back out with a single grep -rl "DEBUG-a4f2". A unique per-session tag is what makes both one-liners work — a shared tag like DEBUG_LAYER collides across sessions and leaves orphaned logs behind. Then investigate the implicated layer in isolation before proposing a fix.
Red Flags
- Fixing a crash without reading the full
Caused by:chain - Guessing at an R8 issue without checking the mapping file
- Adding
Thread.sleep()to "fix" an ANR or race condition - Resolving a dependency conflict by adding
excludewithout understanding why the duplicate exists - Fixing a Compose bug by wrapping in
key()without understanding what triggers recomposition - Leaving temporary instrumentation in the tree — tag every debug log with a unique per-session prefix (e.g.
DEBUG-a4f2) so cleanup is onegrep, and remove them all before declaring the fix done