From personal-skills
Use when debugging Android or KMP issues — supplements superpowers:systematic-debugging with Android-specific techniques covering Logcat, ADB, ANR traces, R8 stack trace decoding, memory leaks, Gradle build failures, and Compose recomposition bugs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/personal-skills:android-debuggingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Extends `superpowers:systematic-debugging` with Android-specific evidence-gathering and investigation techniques.
Extends superpowers:systematic-debugging with Android-specific evidence-gathering and investigation techniques.
REQUIRED BACKGROUND: You MUST follow superpowers:systematic-debugging. Complete Phase 1 (root cause investigation) before attempting any fix. This skill provides Android-specific tools for each phase.
# 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.
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.
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
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.
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).
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"
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.
For deeper Compose performance analysis (stability, recomposition skipping, baseline profiles), see android-skills:compose → references/performance.md.
Wrong state or unexpected re-renders:
android layout --diff gives a JSON tree of what changed between frames.SideEffect { Log.d("Recompose", "MyComposable recomposed") } temporarily to confirmState objects are not created inside the composition (use remember)equals() on state data classes — a new object with same values still triggers recomposition if equals is not implementedNote: 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.
# 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/
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.
Caused by: chainThread.sleep() to "fix" an ANR or race conditionexclude without understanding why the duplicate existskey() without understanding what triggers recompositionDEBUG-a4f2) so cleanup is one grep, and remove them all before declaring the fix doneBuilds a throwaway prototype to answer a design question about UI appearance or state/logic behavior. Guides you through two branches: interactive terminal app for logic validation, or multiple UI variations for visual exploration.
npx claudepluginhub w0lzard/wolzard-s-marketplace --plugin personal-skills