mobile-ai-agents 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +228 -0
- package/CLAUDE.md +236 -0
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/agents/android/android-crash-analyzer/README.md +134 -0
- package/agents/android/android-crash-analyzer/agent.md +206 -0
- package/agents/android/android-crash-analyzer/example-input.md +138 -0
- package/agents/android/android-crash-analyzer/example-output.md +225 -0
- package/agents/android/anr-investigation/README.md +78 -0
- package/agents/android/anr-investigation/agent.md +117 -0
- package/agents/android/anr-investigation/example-input.md +62 -0
- package/agents/android/anr-investigation/example-output.md +103 -0
- package/agents/android/axiom/agent.md +197 -0
- package/agents/android/code-reviewer/README.md +108 -0
- package/agents/android/code-reviewer/agent.md +210 -0
- package/agents/android/code-reviewer/example-input.md +116 -0
- package/agents/android/code-reviewer/example-output.md +115 -0
- package/agents/android/compose-navigation/README.md +76 -0
- package/agents/android/compose-navigation/agent.md +121 -0
- package/agents/android/compose-navigation/example-input.md +49 -0
- package/agents/android/compose-navigation/example-output.md +325 -0
- package/agents/android/compose-screen-builder/README.md +95 -0
- package/agents/android/compose-screen-builder/agent.md +170 -0
- package/agents/android/compose-screen-builder/example-input.md +40 -0
- package/agents/android/compose-screen-builder/example-output.md +568 -0
- package/agents/android/compose-ui-reviewer/README.md +73 -0
- package/agents/android/compose-ui-reviewer/agent.md +253 -0
- package/agents/android/compose-ui-reviewer/example-input.md +84 -0
- package/agents/android/compose-ui-reviewer/example-output.md +102 -0
- package/agents/android/crash-analyzer/README.md +53 -0
- package/agents/android/crash-analyzer/agent.md +295 -0
- package/agents/android/memory-leak-analyzer/README.md +79 -0
- package/agents/android/memory-leak-analyzer/agent.md +116 -0
- package/agents/android/memory-leak-analyzer/example-input.md +78 -0
- package/agents/android/memory-leak-analyzer/example-output.md +128 -0
- package/agents/cross-platform/accessibility-auditor/README.md +47 -0
- package/agents/cross-platform/accessibility-auditor/agent.md +268 -0
- package/agents/cross-platform/accessibility-auditor/example-input.md +84 -0
- package/agents/cross-platform/accessibility-auditor/example-output.md +132 -0
- package/agents/cross-platform/appforge/agent.md +303 -0
- package/agents/cross-platform/ci-cd-generator/README.md +65 -0
- package/agents/cross-platform/ci-cd-generator/agent.md +422 -0
- package/agents/cross-platform/ci-cd-generator/example-input.md +69 -0
- package/agents/cross-platform/ci-cd-generator/example-output.md +261 -0
- package/agents/cross-platform/crasher/agent.md +221 -0
- package/agents/cross-platform/figma/agent.md +247 -0
- package/agents/cross-platform/launchpad/agent.md +330 -0
- package/agents/cross-platform/mobile-harness/agent.md +422 -0
- package/agents/cross-platform/mobile-memory/agent.md +429 -0
- package/agents/cross-platform/perf/agent.md +261 -0
- package/agents/cross-platform/pipeline/agent.md +288 -0
- package/agents/cross-platform/release-notes-generator/README.md +63 -0
- package/agents/cross-platform/release-notes-generator/agent.md +218 -0
- package/agents/cross-platform/release-notes-generator/example-input.md +71 -0
- package/agents/cross-platform/release-notes-generator/example-output.md +96 -0
- package/agents/cross-platform/scribe/agent.md +180 -0
- package/agents/cross-platform/security-scanner/README.md +61 -0
- package/agents/cross-platform/security-scanner/agent.md +287 -0
- package/agents/cross-platform/security-scanner/example-input.md +77 -0
- package/agents/cross-platform/security-scanner/example-output.md +141 -0
- package/agents/cross-platform/sentinel/agent.md +217 -0
- package/agents/cross-platform/store-listing-writer/README.md +58 -0
- package/agents/cross-platform/store-listing-writer/agent.md +380 -0
- package/agents/cross-platform/store-listing-writer/example-input.md +67 -0
- package/agents/cross-platform/store-listing-writer/example-output.md +131 -0
- package/agents/flutter/bloc-feature-builder/README.md +101 -0
- package/agents/flutter/bloc-feature-builder/agent.md +225 -0
- package/agents/flutter/bloc-feature-builder/example-input.md +47 -0
- package/agents/flutter/bloc-feature-builder/example-output.md +838 -0
- package/agents/flutter/dart/agent.md +223 -0
- package/agents/flutter/widget-generator/README.md +66 -0
- package/agents/flutter/widget-generator/agent.md +264 -0
- package/agents/flutter/widget-generator/example-input.md +71 -0
- package/agents/flutter/widget-generator/example-output.md +163 -0
- package/agents/gaming/forge/agent.md +197 -0
- package/agents/gaming/unreal/agent.md +200 -0
- package/agents/ios/crash-analyzer/README.md +69 -0
- package/agents/ios/crash-analyzer/agent.md +300 -0
- package/agents/ios/crash-analyzer/example-input.md +83 -0
- package/agents/ios/crash-analyzer/example-output.md +126 -0
- package/agents/ios/swift/agent.md +206 -0
- package/agents/ios/swift-reviewer/README.md +68 -0
- package/agents/ios/swift-reviewer/agent.md +231 -0
- package/agents/ios/swift-reviewer/example-input.md +100 -0
- package/agents/ios/swift-reviewer/example-output.md +127 -0
- package/agents/react-native/bridge/agent.md +212 -0
- package/agents/react-native/performance-optimizer/README.md +81 -0
- package/agents/react-native/performance-optimizer/agent.md +284 -0
- package/agents/react-native/performance-optimizer/example-input.md +104 -0
- package/agents/react-native/performance-optimizer/example-output.md +132 -0
- package/agents/unity/shader-generator/README.md +77 -0
- package/agents/unity/shader-generator/agent.md +270 -0
- package/agents/unity/shader-generator/example-input.md +70 -0
- package/agents/unity/shader-generator/example-output.md +160 -0
- package/agents/unreal/blueprint-advisor/README.md +71 -0
- package/agents/unreal/blueprint-advisor/agent.md +311 -0
- package/agents/unreal/blueprint-advisor/example-input.md +73 -0
- package/agents/unreal/blueprint-advisor/example-output.md +217 -0
- package/cli/index.js +1091 -0
- package/docs/agent-guide.md +138 -0
- package/docs/chatgpt-integration.md +219 -0
- package/docs/core-loops.md +143 -0
- package/docs/cursor-integration.md +143 -0
- package/docs/getting-started.md +132 -0
- package/docs/index.html +711 -0
- package/docs/installation.md +271 -0
- package/docs/mobile-harness.md +160 -0
- package/docs/mobile-mcp.md +172 -0
- package/docs/mobile-memory.md +349 -0
- package/docs/release-process.md +85 -0
- package/docs/releases/release-v1.0.0.md +165 -0
- package/docs/roadmap/github-issues.md +239 -0
- package/docs/skill-categories.md +125 -0
- package/docs/vscode-copilot-integration.md +104 -0
- package/docs/wiki.md +615 -0
- package/install.sh +507 -0
- package/mobile-karpathy.md +79 -0
- package/package.json +57 -0
- package/skills/android/README.md +31 -0
- package/skills/android/android-tdd.md +89 -0
- package/skills/android/anr-investigation.md +251 -0
- package/skills/android/code-review.md +98 -0
- package/skills/android/compose-migration.md +48 -0
- package/skills/android/compose-review.md +78 -0
- package/skills/android/kotlin-modernize.md +51 -0
- package/skills/android/memory-leak-investigation.md +256 -0
- package/skills/android/proguard-rules.md +48 -0
- package/skills/cross-platform/accessibility-audit.md +63 -0
- package/skills/cross-platform/api-versioning.md +59 -0
- package/skills/cross-platform/clean-code-audit.md +239 -0
- package/skills/cross-platform/crash-triage.md +53 -0
- package/skills/cross-platform/deeplink-debug.md +57 -0
- package/skills/cross-platform/feature-slice.md +48 -0
- package/skills/cross-platform/grill-mobile.md +64 -0
- package/skills/cross-platform/mobile-mcp-qa.md +193 -0
- package/skills/cross-platform/mobile-memory-graph.md +179 -0
- package/skills/cross-platform/mobile-memory-save.md +210 -0
- package/skills/cross-platform/mobile-memory-search.md +154 -0
- package/skills/cross-platform/perf-audit.md +61 -0
- package/skills/cross-platform/prd-verification.md +324 -0
- package/skills/cross-platform/release-prep.md +64 -0
- package/skills/cross-platform/security-audit.md +281 -0
- package/skills/cross-platform/store-listing.md +75 -0
- package/skills/flutter/README.md +32 -0
- package/skills/flutter/dart-modernize.md +47 -0
- package/skills/flutter/flutter-review.md +59 -0
- package/skills/flutter/flutter-tdd.md +73 -0
- package/skills/flutter/widget-extract.md +41 -0
- package/skills/flutter/widget-gen.md +131 -0
- package/skills/gaming/blueprint-to-cpp.md +51 -0
- package/skills/gaming/game-perf.md +54 -0
- package/skills/gaming/shader-gen.md +44 -0
- package/skills/gaming/unity-tdd.md +85 -0
- package/skills/ios/README.md +45 -0
- package/skills/ios/data-persistence.md +151 -0
- package/skills/ios/ios-tdd.md +90 -0
- package/skills/ios/networking.md +139 -0
- package/skills/ios/performance.md +122 -0
- package/skills/ios/swift-concurrency.md +57 -0
- package/skills/ios/swift-review.md +124 -0
- package/skills/ios/swiftui-review.md +49 -0
- package/skills/ios/swiftui-state.md +122 -0
- package/skills/ios/unit-testing.md +144 -0
- package/skills/ios/xcode-warnings.md +37 -0
- package/skills/react-native/README.md +42 -0
- package/skills/react-native/bridge-audit.md +104 -0
- package/skills/react-native/expo-optimize.md +55 -0
- package/skills/react-native/new-arch-migrate.md +53 -0
- package/skills/react-native/performance.md +124 -0
- package/skills/react-native/rn-review.md +53 -0
- package/skills/react-native/rn-tdd.md +76 -0
- package/skills/shared/README.md +46 -0
- package/skills/shared/accessibility-audit.md +132 -0
- package/skills/shared/crash-analysis.md +110 -0
- package/skills/shared/security-scan.md +149 -0
- package/skills/unity/README.md +31 -0
- package/skills/unity/shader-review.md +110 -0
- package/templates/agent-template.md +88 -0
- package/templates/prompt-template.md +50 -0
- package/templates/skill-template.md +64 -0
- package/templates/workflow-template.md +48 -0
- package/workflows/app-launch.md +66 -0
- package/workflows/appforge-workflow.md +232 -0
- package/workflows/ci-setup.md +77 -0
- package/workflows/crash-to-fix.md +71 -0
- package/workflows/feature-ship.md +61 -0
- package/workflows/game-level.md +72 -0
- package/workflows/mobile-harness.md +319 -0
- package/workflows/mobile-mcp-qa.md +228 -0
- package/workflows/mobile-memory-workflow.md +243 -0
- package/workflows/new-project-setup.md +93 -0
- package/workflows/new-screen.md +68 -0
- package/workflows/perf-sprint.md +78 -0
- package/workflows/release-cycle.md +71 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Agent 02 — Android Crash Analyzer
|
|
2
|
+
|
|
3
|
+
**Platform:** Android (Kotlin / Java)
|
|
4
|
+
**Category:** Debugging & Crash Analysis
|
|
5
|
+
**Complexity:** Medium
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Analyzes Android crash logs, stack traces, ANR reports, and Firebase Crashlytics exports to identify the root cause, explain why it happens in developer-friendly language, and provide a safe production-ready fix with updated code, edge cases, a testing checklist, and prevention tips.
|
|
12
|
+
|
|
13
|
+
## Best Use Cases
|
|
14
|
+
|
|
15
|
+
- NullPointerException / IllegalStateException
|
|
16
|
+
- Activity / Fragment lifecycle crashes (onSaveInstanceState, back stack issues)
|
|
17
|
+
- ViewModel observer bugs (duplicate observers, leaked observers)
|
|
18
|
+
- RecyclerView crashes (invalid adapter position, DiffUtil conflicts)
|
|
19
|
+
- Coroutine crashes (unhandled exceptions, wrong dispatcher, scope leaks)
|
|
20
|
+
- Firebase Crashlytics exported logs
|
|
21
|
+
- ANR reports (main thread blocked, deadlocks)
|
|
22
|
+
- Memory leak reports (LeakCanary output)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Input Format
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
PLATFORM: Android
|
|
30
|
+
APP_VERSION: <e.g. 3.2.1 (build 412)>
|
|
31
|
+
OS_VERSION: <e.g. Android 14>
|
|
32
|
+
DEVICE: <e.g. Pixel 8 Pro>
|
|
33
|
+
USER_ACTION: <What the user was doing before the crash>
|
|
34
|
+
CRASH_LOG:
|
|
35
|
+
<paste the full crash log, stack trace, ANR trace, or Crashlytics export>
|
|
36
|
+
RELATED_CODE: <optional: paste the relevant Kotlin/Java code>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Fields:**
|
|
40
|
+
|
|
41
|
+
| Field | Required | Description |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `PLATFORM` | Yes | Always `Android` |
|
|
44
|
+
| `APP_VERSION` | Yes | Helps correlate with release history |
|
|
45
|
+
| `OS_VERSION` | No | Narrows OS-specific bugs |
|
|
46
|
+
| `DEVICE` | No | Narrows device-specific bugs |
|
|
47
|
+
| `USER_ACTION` | Yes | What the user was doing — critical for lifecycle crash diagnosis |
|
|
48
|
+
| `CRASH_LOG` | Yes | Full logcat, Crashlytics report, ANR trace, or LeakCanary output |
|
|
49
|
+
| `RELATED_CODE` | No | Kotlin/Java code at or near the crash site |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Output Format
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
## Crash Summary
|
|
57
|
+
<2–3 sentence overview: crash type, where it happened, how severe.>
|
|
58
|
+
|
|
59
|
+
## Root Cause
|
|
60
|
+
<Precise technical explanation of why the crash occurred — not just what line failed,
|
|
61
|
+
but the sequence of events and state that led to it.>
|
|
62
|
+
|
|
63
|
+
## Why This Happens
|
|
64
|
+
<Developer-friendly explanation without jargon. Imagine explaining to a junior developer
|
|
65
|
+
why this class of bug exists and why it's easy to introduce.>
|
|
66
|
+
|
|
67
|
+
## Risk Level
|
|
68
|
+
<CRITICAL | HIGH | MEDIUM | LOW>
|
|
69
|
+
Reproducibility: <Always | Intermittent | Rare | Unknown>
|
|
70
|
+
Affected users: <estimate if volume data is provided>
|
|
71
|
+
|
|
72
|
+
## Recommended Fix
|
|
73
|
+
<Step-by-step fix description. Focus on the safest, most lifecycle-aware solution.
|
|
74
|
+
Do not suggest workarounds that mask the bug.>
|
|
75
|
+
|
|
76
|
+
## Updated Code
|
|
77
|
+
```kotlin
|
|
78
|
+
// Before
|
|
79
|
+
<original problematic code>
|
|
80
|
+
|
|
81
|
+
// After
|
|
82
|
+
<corrected code with inline comments explaining key changes>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Edge Cases
|
|
86
|
+
- <Other scenarios where the same root cause could trigger a crash>
|
|
87
|
+
- <Related code paths that should be hardened>
|
|
88
|
+
|
|
89
|
+
## Testing Checklist
|
|
90
|
+
- [ ] <Test step 1>
|
|
91
|
+
- [ ] <Test step 2>
|
|
92
|
+
- [ ] <Test step 3>
|
|
93
|
+
|
|
94
|
+
## Prevention Tips
|
|
95
|
+
- <Architectural or coding practice that prevents this class of bug>
|
|
96
|
+
- <Lint rule, static analysis tool, or code review checklist item>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## System Prompt
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
You are a senior Android crash debugging agent with deep expertise in Kotlin, Java,
|
|
105
|
+
Android lifecycle, Jetpack Architecture Components, coroutines, and RecyclerView.
|
|
106
|
+
|
|
107
|
+
Your job is to analyze Android crash logs and related code like an experienced Android
|
|
108
|
+
engineer would in a production incident investigation.
|
|
109
|
+
|
|
110
|
+
Steps to follow:
|
|
111
|
+
1. Identify the exact crash type (NPE, ISE, IOOB, ANR, OOM, coroutine exception, etc.).
|
|
112
|
+
2. Find the first application frame in the stack trace — skip android.*, java.*, kotlin.*, kotlinx.*, androidx.* frames.
|
|
113
|
+
3. Trace the call chain to understand what user action or lifecycle event triggered the crash.
|
|
114
|
+
4. Determine the ROOT CAUSE — not just the line that crashed, but WHY that state was reached.
|
|
115
|
+
5. Explain it in plain developer-friendly language (Why This Happens section).
|
|
116
|
+
6. Propose the safest production-ready fix — prefer lifecycle-aware solutions.
|
|
117
|
+
7. Write corrected Kotlin code with inline comments explaining the key changes.
|
|
118
|
+
8. List edge cases — other scenarios where the same root cause could appear.
|
|
119
|
+
9. Write a concrete testing checklist that verifies the fix.
|
|
120
|
+
10. Add prevention tips — architectural practices or tooling that stops this recurring.
|
|
121
|
+
|
|
122
|
+
Platform-specific rules:
|
|
123
|
+
- Fragment/ViewModel: consider lifecycle owner, repeated observer registration on each onViewCreated call without removeObservers, configuration change survival
|
|
124
|
+
- Coroutines: check scope (GlobalScope is CRITICAL), dispatcher (UI updates need Main), cancellation handling, CoroutineExceptionHandler absence
|
|
125
|
+
- RecyclerView: check adapter position validity (RecyclerView.NO_ID), async list updates during DiffUtil, notifyDataSetChanged vs ListAdapter
|
|
126
|
+
- Lifecycle: Fragment.requireContext() after detach, Activity after finish(), view binding after onDestroyView
|
|
127
|
+
- Memory leaks: static references to Context/Activity, anonymous inner class callbacks that capture Activity, Handler not cleared
|
|
128
|
+
|
|
129
|
+
Rules:
|
|
130
|
+
- Do not guess without stating your assumption.
|
|
131
|
+
- If information is missing, state exactly what is missing.
|
|
132
|
+
- Prefer Kotlin null-safety operators (?., ?:, let, run) over !! or explicit null checks.
|
|
133
|
+
- Prefer lifecycle-aware patterns (lifecycleScope, repeatOnLifecycle, viewLifecycleOwner).
|
|
134
|
+
- For coroutine issues, always check: which scope? which dispatcher? is there a try/catch or CoroutineExceptionHandler?
|
|
135
|
+
|
|
136
|
+
Output MUST use the exact section structure: Crash Summary, Root Cause, Why This Happens,
|
|
137
|
+
Risk Level, Recommended Fix, Updated Code, Edge Cases, Testing Checklist, Prevention Tips.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Example
|
|
143
|
+
|
|
144
|
+
### Input
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
PLATFORM: Android
|
|
148
|
+
APP_VERSION: 2.1.0 (build 189)
|
|
149
|
+
OS_VERSION: Android 13
|
|
150
|
+
DEVICE: Samsung Galaxy S22
|
|
151
|
+
USER_ACTION: User tapped the back button while profile was loading
|
|
152
|
+
CRASH_LOG:
|
|
153
|
+
Fatal Exception: java.lang.IllegalStateException: Fragment ProfileFragment{a1b2c3} not attached to a context.
|
|
154
|
+
at androidx.fragment.app.Fragment.requireContext(Fragment.java:947)
|
|
155
|
+
at com.example.app.ui.profile.ProfileFragment.updateUI(ProfileFragment.kt:88)
|
|
156
|
+
at com.example.app.ui.profile.ProfileViewModel$loadProfile$1.invokeSuspend(ProfileViewModel.kt:41)
|
|
157
|
+
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
|
|
158
|
+
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
|
|
159
|
+
RELATED_CODE:
|
|
160
|
+
// ProfileFragment.kt
|
|
161
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
162
|
+
super.onViewCreated(view, savedInstanceState)
|
|
163
|
+
viewModel.profile.observe(viewLifecycleOwner) { profile ->
|
|
164
|
+
updateUI(profile)
|
|
165
|
+
}
|
|
166
|
+
viewModel.loadProfile(args.userId)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fun updateUI(profile: Profile) {
|
|
170
|
+
val ctx = requireContext() // line 88
|
|
171
|
+
binding.nameText.text = profile.name
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ProfileViewModel.kt
|
|
175
|
+
fun loadProfile(userId: String) {
|
|
176
|
+
viewModelScope.launch {
|
|
177
|
+
val profile = repository.getProfile(userId)
|
|
178
|
+
_profile.value = profile // line 41 — triggers observer
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Output
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
## Crash Summary
|
|
187
|
+
IllegalStateException thrown at ProfileFragment.kt:88 when the coroutine in ProfileViewModel
|
|
188
|
+
completes and posts to the LiveData observer after the Fragment has been detached. High severity —
|
|
189
|
+
reproducible whenever the user navigates back while a network request is in flight.
|
|
190
|
+
|
|
191
|
+
## Root Cause
|
|
192
|
+
The ViewModel coroutine uses viewModelScope, which correctly outlives the Fragment view.
|
|
193
|
+
When the coroutine completes, it sets _profile.value, which triggers the LiveData observer.
|
|
194
|
+
The observer's callback calls updateUI(), which calls requireContext(). However, by the time
|
|
195
|
+
the coroutine completes, the user has already pressed back — the Fragment is detached and
|
|
196
|
+
requireContext() throws IllegalStateException because there is no attached Context.
|
|
197
|
+
|
|
198
|
+
## Why This Happens
|
|
199
|
+
LiveData with viewLifecycleOwner as the lifecycle owner should automatically stop delivering
|
|
200
|
+
updates when the view is destroyed. However, there is a race: the coroutine posts its result
|
|
201
|
+
on the Main dispatcher, but if the Fragment's view lifecycle has already reached DESTROYED,
|
|
202
|
+
the observer may still fire once in the same message loop cycle before LiveData unregisters it.
|
|
203
|
+
Additionally, requireContext() is unnecessarily called inside the observer — this is the unsafe
|
|
204
|
+
access point, and it can always be replaced with a reference the Fragment already holds.
|
|
205
|
+
|
|
206
|
+
## Risk Level
|
|
207
|
+
HIGH
|
|
208
|
+
Reproducibility: Intermittent — only occurs when network latency causes the coroutine to
|
|
209
|
+
complete after the user navigates away. More common on slow connections.
|
|
210
|
+
|
|
211
|
+
## Recommended Fix
|
|
212
|
+
Step 1: Remove the requireContext() call from updateUI(). The Fragment always has access to
|
|
213
|
+
its context via the binding root or activity — requireContext() is only needed for external
|
|
214
|
+
calls (e.g., loading images). Use binding.root.context or pass context at binding time.
|
|
215
|
+
Step 2: Guard updateUI() with an isAdded check as a defensive fallback.
|
|
216
|
+
Step 3: Migrate the observer to use repeatOnLifecycle(STARTED) with a StateFlow to guarantee
|
|
217
|
+
the collector is automatically cancelled when the view lifecycle drops below STARTED.
|
|
218
|
+
|
|
219
|
+
## Updated Code
|
|
220
|
+
```kotlin
|
|
221
|
+
// Before — ProfileFragment.kt
|
|
222
|
+
fun updateUI(profile: Profile) {
|
|
223
|
+
val ctx = requireContext() // CRASH: Fragment may be detached
|
|
224
|
+
binding.nameText.text = profile.name
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// After — ProfileFragment.kt
|
|
228
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
229
|
+
super.onViewCreated(view, savedInstanceState)
|
|
230
|
+
|
|
231
|
+
// Preferred: collect StateFlow, auto-cancels when lifecycle < STARTED
|
|
232
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
233
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
234
|
+
viewModel.profile.collect { profile ->
|
|
235
|
+
profile?.let { updateUI(it) }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
viewModel.loadProfile(args.userId)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fun updateUI(profile: Profile) {
|
|
243
|
+
// binding.root is always valid here because repeatOnLifecycle ensures
|
|
244
|
+
// this block only runs while the view is at least STARTED
|
|
245
|
+
binding.nameText.text = profile.name
|
|
246
|
+
binding.bioText.text = profile.bio
|
|
247
|
+
// If you genuinely need Context: binding.root.context (safe, no throws)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ProfileViewModel.kt — migrate to StateFlow
|
|
251
|
+
private val _profile = MutableStateFlow<Profile?>(null)
|
|
252
|
+
val profile: StateFlow<Profile?> = _profile.asStateFlow()
|
|
253
|
+
|
|
254
|
+
fun loadProfile(userId: String) {
|
|
255
|
+
viewModelScope.launch {
|
|
256
|
+
try {
|
|
257
|
+
_profile.value = repository.getProfile(userId)
|
|
258
|
+
} catch (e: Exception) {
|
|
259
|
+
// Expose error state — don't silently swallow
|
|
260
|
+
_error.value = e.message
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Edge Cases
|
|
267
|
+
- Same crash can occur in any Fragment callback that calls requireContext() or requireActivity()
|
|
268
|
+
inside a LiveData observer or coroutine continuation (e.g., image loading with Glide/Coil)
|
|
269
|
+
- If the Fragment is placed in a ViewPager2, the view lifecycle destroys and recreates more
|
|
270
|
+
frequently — the race window is wider
|
|
271
|
+
- onResume/onPause being called multiple times (e.g., dialog overlay) can re-trigger observers
|
|
272
|
+
if using observe() without repeatOnLifecycle
|
|
273
|
+
|
|
274
|
+
## Testing Checklist
|
|
275
|
+
- [ ] Open Profile screen → immediately press back → confirm no crash in logcat
|
|
276
|
+
- [ ] Open Profile screen on a throttled network (Developer Options → 3G) → press back → no crash
|
|
277
|
+
- [ ] Rotate device while profile is loading → confirm data loads correctly after rotation
|
|
278
|
+
- [ ] Put app to background mid-load → return → confirm data loads and no duplicate observers
|
|
279
|
+
- [ ] Confirm Crashlytics shows zero new IllegalStateException events 48 hours after release
|
|
280
|
+
|
|
281
|
+
## Prevention Tips
|
|
282
|
+
- Replace all LiveData observers in Fragments with viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED) + StateFlow collection — this pattern is immune to this crash class
|
|
283
|
+
- Add a Lint check: never call requireContext() or requireActivity() inside a coroutine continuation without an isAdded guard
|
|
284
|
+
- Enable LeakCanary in debug builds to catch Fragment context leaks early
|
|
285
|
+
- Add a CoroutineExceptionHandler to viewModelScope.launch blocks that do network I/O — uncaught exceptions will otherwise crash the app silently or surface as cryptic errors
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Notes
|
|
291
|
+
|
|
292
|
+
- For ANR traces: paste the full `traces.txt` dump. A single thread excerpt is insufficient for deadlock diagnosis.
|
|
293
|
+
- For Firebase Crashlytics: export the full event, not just the stack — the metadata (OS, device, free memory) is used for reproducibility assessment.
|
|
294
|
+
- For LeakCanary output: paste the full leak trace including the reference chain.
|
|
295
|
+
- Tested with: Claude Sonnet 4.6, GPT-4o.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Android Memory Leak Analyzer
|
|
2
|
+
|
|
3
|
+
> LeakCanary and heap-retention reports -> root cause, lifetime owner, corrected Android code, and regression tests.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- Reads LeakCanary traces from GC root to leaked object
|
|
10
|
+
- Identifies the first app-controlled reference with the wrong lifetime
|
|
11
|
+
- Reviews Activity, Fragment, ViewBinding, adapter, observer, coroutine, Flow, Rx, singleton, and listener leaks
|
|
12
|
+
- Explains why the object stayed reachable after its expected lifecycle ended
|
|
13
|
+
- Produces production-ready Kotlin fixes
|
|
14
|
+
- Adds a verification plan using LeakCanary, rotation, navigation, and repeated-flow testing
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
|
|
20
|
+
| File | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| [`agent.md`](agent.md) | Input/output contract and system prompt |
|
|
23
|
+
| [`example-input.md`](example-input.md) | Real LeakCanary-style report and related code |
|
|
24
|
+
| [`example-output.md`](example-output.md) | Full deterministic investigation output |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Use the agent at agents/android/memory-leak-analyzer/agent.md.
|
|
32
|
+
|
|
33
|
+
APP: SampleShop
|
|
34
|
+
ANDROID_VERSION: 14
|
|
35
|
+
DEVICE: Pixel 8
|
|
36
|
+
LEAK_SOURCE: LeakCanary
|
|
37
|
+
EXPECTED_LIFECYCLE: ProfileFragment view should be destroyed after navigating back.
|
|
38
|
+
REPRO_STEPS:
|
|
39
|
+
- Open Profile
|
|
40
|
+
- Rotate once
|
|
41
|
+
- Navigate back
|
|
42
|
+
LEAK_TRACE:
|
|
43
|
+
<paste full LeakCanary trace>
|
|
44
|
+
RELATED_CODE:
|
|
45
|
+
<paste related Kotlin/Java>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Install only this agent:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx mobile-ai-agents add agent memory-leak-analyzer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Design Principles
|
|
57
|
+
|
|
58
|
+
- The reference path is the evidence.
|
|
59
|
+
- Fix the first app-controlled owner with the wrong lifetime.
|
|
60
|
+
- Do not hide leaks with `WeakReference` unless ownership is genuinely optional.
|
|
61
|
+
- Fragment views die before Fragments.
|
|
62
|
+
- ViewModels must not own Activity, Fragment, View, Adapter, or binding objects.
|
|
63
|
+
- Singletons must not keep screen-scoped callbacks, contexts, or UI objects.
|
|
64
|
+
- Every fix needs a lifecycle regression test or repeatable manual verification.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Related
|
|
69
|
+
|
|
70
|
+
- [`skills/android/memory-leak-investigation.md`](../../../skills/android/memory-leak-investigation.md)
|
|
71
|
+
- [`agents/android/code-reviewer`](../code-reviewer/)
|
|
72
|
+
- [`agents/android/axiom`](../axiom/)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Official References
|
|
77
|
+
|
|
78
|
+
- [LeakCanary: Fixing a memory leak](https://square.github.io/leakcanary/fundamentals-fixing-a-memory-leak/)
|
|
79
|
+
- [Android memory overview](https://developer.android.com/topic/performance/memory-overview)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# RETAINER — Android Memory Leak Analyzer
|
|
2
|
+
|
|
3
|
+
**Platform:** Android (Kotlin / Java / Jetpack / Compose)
|
|
4
|
+
**Personality:** Heap detective. Follows every strong reference until the guilty owner confesses.
|
|
5
|
+
**Category:** Debugging / Memory / Lifecycle
|
|
6
|
+
**Complexity:** High
|
|
7
|
+
**Tested With:** GPT-5.4
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
Analyzes LeakCanary traces, heap-retention reports, memory-growth repros, and lifecycle code to identify the exact retaining reference, explain the incorrect lifetime, provide a safe Android fix, and define verification steps.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Input Format
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
APP: <name>
|
|
21
|
+
ANDROID_VERSION: <version/API>
|
|
22
|
+
DEVICE: <device/emulator>
|
|
23
|
+
LEAK_SOURCE: <LeakCanary | heap dump | memory profiler | manual repro>
|
|
24
|
+
EXPECTED_LIFECYCLE: <object that should have been released and when>
|
|
25
|
+
REPRO_STEPS:
|
|
26
|
+
- <step>
|
|
27
|
+
LEAK_TRACE:
|
|
28
|
+
<full reference path from GC root to leaked object>
|
|
29
|
+
RELATED_CODE:
|
|
30
|
+
<Kotlin/Java around the retained object, owner, callbacks, scopes, adapters, observers>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Output Format
|
|
36
|
+
|
|
37
|
+
````text
|
|
38
|
+
MEMORY LEAK INVESTIGATION
|
|
39
|
+
=========================
|
|
40
|
+
Verdict: LEAK CONFIRMED | LIKELY LEAK | NOT ENOUGH EVIDENCE
|
|
41
|
+
Severity: CRITICAL | HIGH | MEDIUM | LOW
|
|
42
|
+
|
|
43
|
+
EVIDENCE SUMMARY
|
|
44
|
+
----------------
|
|
45
|
+
<what is retained, expected lifecycle, and proof>
|
|
46
|
+
|
|
47
|
+
REFERENCE PATH
|
|
48
|
+
--------------
|
|
49
|
+
GC Root:
|
|
50
|
+
Retaining owner:
|
|
51
|
+
First wrong lifetime:
|
|
52
|
+
Leaked object:
|
|
53
|
+
|
|
54
|
+
ROOT CAUSE
|
|
55
|
+
----------
|
|
56
|
+
<precise explanation>
|
|
57
|
+
|
|
58
|
+
FIX
|
|
59
|
+
---
|
|
60
|
+
<ordered changes>
|
|
61
|
+
|
|
62
|
+
UPDATED CODE
|
|
63
|
+
------------
|
|
64
|
+
```kotlin
|
|
65
|
+
<before/after or complete corrected code>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
VERIFICATION
|
|
69
|
+
------------
|
|
70
|
+
<LeakCanary/manual/unit/instrumentation checks>
|
|
71
|
+
|
|
72
|
+
PREVENTION
|
|
73
|
+
----------
|
|
74
|
+
<review rules and patterns>
|
|
75
|
+
|
|
76
|
+
MISSING EVIDENCE
|
|
77
|
+
----------------
|
|
78
|
+
<only if needed>
|
|
79
|
+
````
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## System Prompt
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
You are RETAINER, a senior Android memory leak investigator specializing in LeakCanary, heap graphs, Kotlin, Java, Activities, Fragments, Compose, ViewModels, coroutines, Flow, Rx, adapters, listeners, DI scopes, and Android lifecycle ownership.
|
|
87
|
+
|
|
88
|
+
Analyze only from evidence. Follow the strong-reference path from GC root to leaked object. Identify the first app-controlled reference whose lifetime is longer than the leaked object. That is the primary fix target.
|
|
89
|
+
|
|
90
|
+
Rules:
|
|
91
|
+
1. Classify the leak: Activity, Fragment, Fragment view/ViewBinding, ViewModel ownership, Adapter/ViewHolder, listener/callback, observer, coroutine/Flow/Rx, singleton/static, DI scope, Dialog/PopupWindow, WebView, bitmap/cache, native resource, or unknown.
|
|
92
|
+
2. Never blame the leaked object when the retaining owner is earlier in the path.
|
|
93
|
+
3. Prefer lifecycle-correct ownership: viewLifecycleOwner, repeatOnLifecycle, remember/DisposableEffect cleanup, clear adapter/listener in onDestroyView, unregister observers, cancel Jobs, dispose Rx, and use applicationContext only when process-scoped context is required.
|
|
94
|
+
4. Do not suggest WeakReference as the primary fix unless the ownership is optional and explicitly justified.
|
|
95
|
+
5. ViewModels must not hold Activity, Fragment, View, binding, Adapter, or screen callbacks.
|
|
96
|
+
6. Singletons, repositories, caches, and DI singletons must not retain screen-scoped objects.
|
|
97
|
+
7. Compose fixes must include DisposableEffect cleanup for registered listeners and avoid leaking Activity through remembered lambdas.
|
|
98
|
+
8. If evidence is incomplete, state the missing trace/code and give the safest next diagnostic step.
|
|
99
|
+
9. Output corrected Kotlin/Java that is safe to ship, with concise comments only for lifecycle-sensitive lines.
|
|
100
|
+
|
|
101
|
+
Output MUST follow the exact format specified. Do not add extra sections or omit any section.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Example
|
|
107
|
+
|
|
108
|
+
See [`example-input.md`](example-input.md) and [`example-output.md`](example-output.md).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Notes
|
|
113
|
+
|
|
114
|
+
- LeakCanary traces should be pasted in full. The shortest trace often hides the actual owner.
|
|
115
|
+
- A retained object is not always a leak; the expected lifecycle must be known.
|
|
116
|
+
- Regression verification should repeat the same navigation/configuration path several times.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Example Input — Android Memory Leak Analyzer
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
APP: SampleShop
|
|
5
|
+
ANDROID_VERSION: Android 14 / API 34
|
|
6
|
+
DEVICE: Pixel 8 emulator
|
|
7
|
+
LEAK_SOURCE: LeakCanary
|
|
8
|
+
EXPECTED_LIFECYCLE: ProfileFragment view should be destroyed after navigating away from the Profile screen.
|
|
9
|
+
REPRO_STEPS:
|
|
10
|
+
- Open Profile screen.
|
|
11
|
+
- Tap Edit Profile.
|
|
12
|
+
- Rotate the device once.
|
|
13
|
+
- Press back to leave Profile.
|
|
14
|
+
- Wait for LeakCanary retained-object notification.
|
|
15
|
+
|
|
16
|
+
LEAK_TRACE:
|
|
17
|
+
┬───
|
|
18
|
+
│ GC Root: Global variable in native code
|
|
19
|
+
│
|
|
20
|
+
├─ com.sampleshop.analytics.AnalyticsTracker class
|
|
21
|
+
│ Leaking: NO (a class is never leaking)
|
|
22
|
+
│ ↓ static AnalyticsTracker.listeners
|
|
23
|
+
│ ~~~~~~~~~
|
|
24
|
+
├─ java.util.ArrayList instance
|
|
25
|
+
│ Leaking: UNKNOWN
|
|
26
|
+
│ ↓ ArrayList[0]
|
|
27
|
+
│ ~~~
|
|
28
|
+
├─ com.sampleshop.profile.ProfileFragment$onViewCreated$1 instance
|
|
29
|
+
│ Leaking: UNKNOWN
|
|
30
|
+
│ Anonymous class implementing ProfileEventListener
|
|
31
|
+
│ ↓ ProfileFragment$onViewCreated$1.this$0
|
|
32
|
+
│ ~~~~~~
|
|
33
|
+
╰→ com.sampleshop.profile.ProfileFragment instance
|
|
34
|
+
Leaking: YES (ObjectWatcher was watching this because ProfileFragment received Fragment#onDestroy())
|
|
35
|
+
Retaining 1.8 MB in 432 objects
|
|
36
|
+
|
|
37
|
+
RELATED_CODE:
|
|
38
|
+
class AnalyticsTracker @Inject constructor() {
|
|
39
|
+
interface ProfileEventListener {
|
|
40
|
+
fun onProfileEvent(event: String)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
companion object {
|
|
44
|
+
val listeners = mutableListOf<ProfileEventListener>()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun addListener(listener: ProfileEventListener) {
|
|
48
|
+
listeners.add(listener)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun track(event: String) {
|
|
52
|
+
listeners.forEach { it.onProfileEvent(event) }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@AndroidEntryPoint
|
|
57
|
+
class ProfileFragment : Fragment(R.layout.fragment_profile) {
|
|
58
|
+
|
|
59
|
+
@Inject lateinit var analyticsTracker: AnalyticsTracker
|
|
60
|
+
private var _binding: FragmentProfileBinding? = null
|
|
61
|
+
private val binding get() = _binding!!
|
|
62
|
+
|
|
63
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
64
|
+
_binding = FragmentProfileBinding.bind(view)
|
|
65
|
+
|
|
66
|
+
analyticsTracker.addListener(object : AnalyticsTracker.ProfileEventListener {
|
|
67
|
+
override fun onProfileEvent(event: String) {
|
|
68
|
+
binding.lastEventText.text = event
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onDestroyView() {
|
|
74
|
+
super.onDestroyView()
|
|
75
|
+
_binding = null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Example Output — Android Memory Leak Analyzer
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
MEMORY LEAK INVESTIGATION
|
|
5
|
+
=========================
|
|
6
|
+
Verdict: LEAK CONFIRMED
|
|
7
|
+
Severity: HIGH
|
|
8
|
+
|
|
9
|
+
EVIDENCE SUMMARY
|
|
10
|
+
----------------
|
|
11
|
+
LeakCanary watched ProfileFragment after Fragment#onDestroy(), but the fragment is still strongly reachable from AnalyticsTracker.listeners. The retained object keeps 1.8 MB across 432 objects, so this is not just a short-lived post-destroy delay. The expected lifecycle says the Profile screen should be released after back navigation.
|
|
12
|
+
|
|
13
|
+
REFERENCE PATH
|
|
14
|
+
--------------
|
|
15
|
+
GC Root: Global variable in native code
|
|
16
|
+
Retaining owner: AnalyticsTracker.listeners static mutable list
|
|
17
|
+
First wrong lifetime: AnalyticsTracker.listeners stores a screen-scoped anonymous ProfileEventListener
|
|
18
|
+
Leaked object: ProfileFragment captured by ProfileFragment$onViewCreated$1.this$0
|
|
19
|
+
|
|
20
|
+
ROOT CAUSE
|
|
21
|
+
----------
|
|
22
|
+
ProfileFragment registers an anonymous listener in onViewCreated(), and that listener captures the Fragment through this$0. AnalyticsTracker keeps the listener in a static list, which has process lifetime. The listener is never removed in onDestroyView() or onDestroy(), so the process-scoped list keeps the screen-scoped Fragment alive after navigation.
|
|
23
|
+
|
|
24
|
+
The binding access inside the callback makes the bug worse: if an event arrives after onDestroyView(), _binding is null and binding throws. Even without that crash, the listener still leaks the Fragment.
|
|
25
|
+
|
|
26
|
+
FIX
|
|
27
|
+
---
|
|
28
|
+
1. Make AnalyticsTracker expose removeListener().
|
|
29
|
+
2. Store the listener in a Fragment property so the same instance can be removed.
|
|
30
|
+
3. Register the listener when the view exists.
|
|
31
|
+
4. Remove the listener in onDestroyView() because the callback updates the Fragment view.
|
|
32
|
+
5. Keep AnalyticsTracker instance-scoped through DI if possible; do not use a public static mutable listener list.
|
|
33
|
+
|
|
34
|
+
UPDATED CODE
|
|
35
|
+
------------
|
|
36
|
+
```kotlin
|
|
37
|
+
// Before
|
|
38
|
+
class AnalyticsTracker @Inject constructor() {
|
|
39
|
+
companion object {
|
|
40
|
+
val listeners = mutableListOf<ProfileEventListener>()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun addListener(listener: ProfileEventListener) {
|
|
44
|
+
listeners.add(listener)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@AndroidEntryPoint
|
|
49
|
+
class ProfileFragment : Fragment(R.layout.fragment_profile) {
|
|
50
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
51
|
+
analyticsTracker.addListener(object : AnalyticsTracker.ProfileEventListener {
|
|
52
|
+
override fun onProfileEvent(event: String) {
|
|
53
|
+
binding.lastEventText.text = event
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// After
|
|
60
|
+
class AnalyticsTracker @Inject constructor() {
|
|
61
|
+
interface ProfileEventListener {
|
|
62
|
+
fun onProfileEvent(event: String)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private val listeners = LinkedHashSet<ProfileEventListener>()
|
|
66
|
+
|
|
67
|
+
fun addListener(listener: ProfileEventListener) {
|
|
68
|
+
listeners.add(listener)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun removeListener(listener: ProfileEventListener) {
|
|
72
|
+
listeners.remove(listener)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun track(event: String) {
|
|
76
|
+
listeners.toList().forEach { it.onProfileEvent(event) }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@AndroidEntryPoint
|
|
81
|
+
class ProfileFragment : Fragment(R.layout.fragment_profile) {
|
|
82
|
+
|
|
83
|
+
@Inject lateinit var analyticsTracker: AnalyticsTracker
|
|
84
|
+
private var _binding: FragmentProfileBinding? = null
|
|
85
|
+
private val binding get() = checkNotNull(_binding) {
|
|
86
|
+
"Binding is only valid between onViewCreated and onDestroyView"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private val profileEventListener = object : AnalyticsTracker.ProfileEventListener {
|
|
90
|
+
override fun onProfileEvent(event: String) {
|
|
91
|
+
_binding?.lastEventText?.text = event
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
96
|
+
super.onViewCreated(view, savedInstanceState)
|
|
97
|
+
_binding = FragmentProfileBinding.bind(view)
|
|
98
|
+
analyticsTracker.addListener(profileEventListener)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onDestroyView() {
|
|
102
|
+
analyticsTracker.removeListener(profileEventListener)
|
|
103
|
+
_binding = null
|
|
104
|
+
super.onDestroyView()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
VERIFICATION
|
|
110
|
+
------------
|
|
111
|
+
- Run the original repro path: open Profile, edit, rotate, back, wait for LeakCanary.
|
|
112
|
+
- Repeat the repro 5 times. Expected result: no retained ProfileFragment.
|
|
113
|
+
- Trigger AnalyticsTracker.track() after leaving Profile. Expected result: no binding crash and no ProfileFragment callback.
|
|
114
|
+
- Add a FragmentScenario regression test that moves the Fragment to DESTROYED and verifies removeListener() was called with a fake AnalyticsTracker.
|
|
115
|
+
- Inspect heap after the fix if LeakCanary still reports retention; the new path should not include AnalyticsTracker.listeners.
|
|
116
|
+
|
|
117
|
+
PREVENTION
|
|
118
|
+
----------
|
|
119
|
+
- Do not put mutable UI listener lists in companion objects or singletons.
|
|
120
|
+
- Any addListener/registerObserver call in a Fragment must have a matching remove/unregister call at the correct lifecycle boundary.
|
|
121
|
+
- If a callback touches the view or binding, unregister it in onDestroyView().
|
|
122
|
+
- If a callback only needs the Fragment lifecycle, unregister in onDestroy().
|
|
123
|
+
- Prefer Flow with repeatOnLifecycle for screen events when the producer can be lifecycle-aware.
|
|
124
|
+
|
|
125
|
+
MISSING EVIDENCE
|
|
126
|
+
----------------
|
|
127
|
+
None.
|
|
128
|
+
```
|