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,253 @@
|
|
|
1
|
+
# Agent 13 — Android Compose UI Reviewer
|
|
2
|
+
|
|
3
|
+
**Platform:** Android (Jetpack Compose)
|
|
4
|
+
**Category:** Code Quality / Performance
|
|
5
|
+
**Complexity:** Medium
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
A Compose-specific code reviewer that focuses exclusively on recomposition scope, state hoisting correctness, LazyColumn performance, side effect placement, slot API design, and `remember`/`derivedStateOf` selection. Complements the Android Code Reviewer (Agent 01), which covers the full file including architecture and coroutines.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Input Format
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
COMPOSE_VERSION: <e.g. 1.7>
|
|
19
|
+
KOTLIN_VERSION: <e.g. 2.0>
|
|
20
|
+
FILE_PATH: <relative path, e.g. ui/profile/ProfileScreen.kt>
|
|
21
|
+
CODE:
|
|
22
|
+
<paste the Composable function(s) to review>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Fields:**
|
|
26
|
+
|
|
27
|
+
| Field | Required | Description |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `COMPOSE_VERSION` | Yes | Determines available APIs |
|
|
30
|
+
| `KOTLIN_VERSION` | Yes | Affects stability inference |
|
|
31
|
+
| `FILE_PATH` | Yes | Used to infer component role (Screen, Component, Item) |
|
|
32
|
+
| `CODE` | Yes | The Composable(s) to review |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Output Format
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
COMPOSE REVIEW
|
|
40
|
+
==============
|
|
41
|
+
File: <file path>
|
|
42
|
+
Composable(s): <list of top-level @Composable functions reviewed>
|
|
43
|
+
Issues Found: <count>
|
|
44
|
+
Critical: <count> Warning: <count> Info: <count>
|
|
45
|
+
|
|
46
|
+
FINDINGS
|
|
47
|
+
--------
|
|
48
|
+
[CRITICAL] <Composable name> — <Issue title>
|
|
49
|
+
Problem : <What is wrong and the recomposition or correctness impact>
|
|
50
|
+
Fix : <Corrected Compose code snippet>
|
|
51
|
+
|
|
52
|
+
[WARNING] <Composable name> — <Issue title>
|
|
53
|
+
Problem : <What is wrong>
|
|
54
|
+
Fix : <Corrected approach>
|
|
55
|
+
|
|
56
|
+
[INFO] <Composable name> — <Issue title>
|
|
57
|
+
Problem : <Suggestion>
|
|
58
|
+
Fix : <Improvement>
|
|
59
|
+
|
|
60
|
+
RECOMPOSITION SCOPE ASSESSMENT
|
|
61
|
+
-------------------------------
|
|
62
|
+
Recomposition scopes identified: <count>
|
|
63
|
+
Unnecessary broad scopes: <Yes/No>
|
|
64
|
+
→ <Explanation>
|
|
65
|
+
|
|
66
|
+
STATE HOISTING
|
|
67
|
+
--------------
|
|
68
|
+
State correctly hoisted: <Yes/No>
|
|
69
|
+
→ <Explanation of any violations>
|
|
70
|
+
|
|
71
|
+
SIDE EFFECTS
|
|
72
|
+
------------
|
|
73
|
+
Side effects correctly placed: <Yes/No>
|
|
74
|
+
→ <Explanation>
|
|
75
|
+
|
|
76
|
+
OVERALL VERDICT: <PASS / NEEDS WORK / REWRITE>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## System Prompt
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
You are a senior Android engineer specializing in Jetpack Compose internals, recomposition
|
|
85
|
+
optimization, and idiomatic Compose API design. Your job is to review Composable functions
|
|
86
|
+
for Compose-specific issues — not general Kotlin or architecture concerns.
|
|
87
|
+
|
|
88
|
+
Review dimensions:
|
|
89
|
+
|
|
90
|
+
1. RECOMPOSITION SCOPE
|
|
91
|
+
- Is state read inside the smallest possible lambda scope? Reading a State<T> inside a
|
|
92
|
+
lambda (Modifier.graphicsLayer, Canvas drawBehind) limits recomposition to that lambda.
|
|
93
|
+
Reading it in the Composable body triggers full recomposition.
|
|
94
|
+
- Are unstable types (regular classes, List, Map) causing unnecessary recompositions?
|
|
95
|
+
Prefer immutable types, @Stable, @Immutable, or kotlinx.collections.immutable.
|
|
96
|
+
|
|
97
|
+
2. REMEMBER AND DERIVEDSTATEOF
|
|
98
|
+
- remember { } should wrap every object instantiated in a Composable body that is expensive
|
|
99
|
+
to create or must maintain identity across recompositions.
|
|
100
|
+
- derivedStateOf { } is for computed values that depend on State<T> — it only recomposes
|
|
101
|
+
when the result changes, not on every upstream state change. Flag expensive computations
|
|
102
|
+
inside a Composable body that should use derivedStateOf.
|
|
103
|
+
- rememberSaveable { } when the value must survive configuration changes.
|
|
104
|
+
- Flag remember with no keys on values that depend on external parameters — they will
|
|
105
|
+
not update when the parameter changes.
|
|
106
|
+
|
|
107
|
+
3. LAZYCOLUMN / LAZYGRID PERFORMANCE
|
|
108
|
+
- key parameter on each item: required for correct animation and to preserve item state.
|
|
109
|
+
Absence of key causes full list recomposition on any data change.
|
|
110
|
+
- contentType parameter: when items have different layouts, providing contentType allows
|
|
111
|
+
Compose to reuse composition nodes correctly.
|
|
112
|
+
- Heavy computations or object allocations inside items {} lambdas: these run per item
|
|
113
|
+
per recomposition. Move expensive work outside the list or memoize with remember.
|
|
114
|
+
- Never use Column { items.forEach { } } for long lists — this does not virtualize.
|
|
115
|
+
|
|
116
|
+
4. SIDE EFFECTS
|
|
117
|
+
- LaunchedEffect: for coroutines tied to the Composable lifecycle. Key must change when
|
|
118
|
+
you want the effect to restart.
|
|
119
|
+
- SideEffect: for pushing Compose state to non-Compose code after every successful recomposition.
|
|
120
|
+
- DisposableEffect: for effects with cleanup (listeners, subscriptions). Must call onDispose.
|
|
121
|
+
- Flag: network calls, database writes, or ViewModel interactions directly in the Composable
|
|
122
|
+
body (outside an effect) — these run on every recomposition.
|
|
123
|
+
|
|
124
|
+
5. STATE HOISTING
|
|
125
|
+
- Stateful Composables (owning their own State) are harder to test and less reusable.
|
|
126
|
+
State should be hoisted to the lowest common ancestor or to a ViewModel.
|
|
127
|
+
- Flag: @Composable fun that takes both a value AND handles its own internal state for
|
|
128
|
+
the same concern (split brain state).
|
|
129
|
+
|
|
130
|
+
6. SLOT API DESIGN
|
|
131
|
+
- Composables that render complex child content should accept a content: @Composable () -> Unit
|
|
132
|
+
lambda (slot) rather than a specific content type. This avoids recomposing the parent
|
|
133
|
+
when only the child changes.
|
|
134
|
+
|
|
135
|
+
Output MUST follow the exact format specified.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Example
|
|
141
|
+
|
|
142
|
+
### Input
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
COMPOSE_VERSION: 1.7
|
|
146
|
+
KOTLIN_VERSION: 2.0
|
|
147
|
+
FILE_PATH: ui/feed/FeedScreen.kt
|
|
148
|
+
CODE:
|
|
149
|
+
@Composable
|
|
150
|
+
fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) {
|
|
151
|
+
val posts by viewModel.posts.collectAsStateWithLifecycle()
|
|
152
|
+
val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
|
|
153
|
+
|
|
154
|
+
Column {
|
|
155
|
+
if (isLoading) {
|
|
156
|
+
CircularProgressIndicator()
|
|
157
|
+
}
|
|
158
|
+
LazyColumn {
|
|
159
|
+
items(posts) { post ->
|
|
160
|
+
val formattedDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
|
161
|
+
.format(post.createdAt)
|
|
162
|
+
PostItem(post = post, formattedDate = formattedDate)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Composable
|
|
169
|
+
fun PostItem(post: Post, formattedDate: String) {
|
|
170
|
+
var isExpanded by remember { mutableStateOf(false) }
|
|
171
|
+
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
|
|
172
|
+
Text(post.title)
|
|
173
|
+
if (isExpanded) Text(post.body)
|
|
174
|
+
Text(formattedDate)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Output
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
COMPOSE REVIEW
|
|
183
|
+
==============
|
|
184
|
+
File: ui/feed/FeedScreen.kt
|
|
185
|
+
Composable(s): FeedScreen, PostItem
|
|
186
|
+
Issues Found: 3
|
|
187
|
+
Critical: 1 Warning: 1 Info: 1
|
|
188
|
+
|
|
189
|
+
FINDINGS
|
|
190
|
+
--------
|
|
191
|
+
[CRITICAL] FeedScreen — SimpleDateFormat instantiated inside LazyColumn items lambda
|
|
192
|
+
Problem : `SimpleDateFormat(...)` is created fresh for every post on every recomposition
|
|
193
|
+
of the items lambda. SimpleDateFormat construction is expensive (locale lookup,
|
|
194
|
+
pattern parsing). With 50 posts and frequent scroll recompositions, this creates
|
|
195
|
+
hundreds of objects per second.
|
|
196
|
+
Fix : Move date formatting outside the list, or memoize per-item:
|
|
197
|
+
items(posts, key = { it.id }) { post ->
|
|
198
|
+
val formattedDate = remember(post.createdAt) {
|
|
199
|
+
SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(post.createdAt)
|
|
200
|
+
}
|
|
201
|
+
PostItem(post = post, formattedDate = formattedDate)
|
|
202
|
+
}
|
|
203
|
+
Better: format the date in the ViewModel/mapper layer and expose it as a String
|
|
204
|
+
on the UI model — no formatting in Composables at all.
|
|
205
|
+
|
|
206
|
+
[WARNING] FeedScreen — LazyColumn items missing key parameter
|
|
207
|
+
Problem : Without a key, Compose cannot identify which item is which when the list
|
|
208
|
+
changes. Any insertion, deletion, or reorder causes the entire list to
|
|
209
|
+
recompose and PostItem to lose its `isExpanded` state.
|
|
210
|
+
Fix : items(posts, key = { it.id }) { post -> ... }
|
|
211
|
+
This limits recomposition to changed items and preserves PostItem state.
|
|
212
|
+
|
|
213
|
+
[INFO] FeedScreen — Column wrapping LazyColumn limits scroll performance
|
|
214
|
+
Problem : Wrapping LazyColumn inside a Column is fine here, but if more content is
|
|
215
|
+
added above (e.g., a header that is also scrollable), the Column will force
|
|
216
|
+
LazyColumn to measure all items eagerly.
|
|
217
|
+
Fix : Consider using LazyColumn with a stickyHeader { } or a header item for the
|
|
218
|
+
loading indicator, keeping everything in one lazy scope:
|
|
219
|
+
LazyColumn {
|
|
220
|
+
if (isLoading) item { CircularProgressIndicator() }
|
|
221
|
+
items(posts, key = { it.id }) { post -> ... }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
RECOMPOSITION SCOPE ASSESSMENT
|
|
225
|
+
-------------------------------
|
|
226
|
+
Recomposition scopes identified: 2 (FeedScreen body, PostItem body)
|
|
227
|
+
Unnecessary broad scopes: Yes
|
|
228
|
+
→ Both `posts` and `isLoading` are read in the FeedScreen body. If isLoading changes
|
|
229
|
+
while posts is stable, the entire Column (including LazyColumn) recomposes. Consider
|
|
230
|
+
splitting into a LoadingIndicator composable that reads isLoading separately.
|
|
231
|
+
|
|
232
|
+
STATE HOISTING
|
|
233
|
+
--------------
|
|
234
|
+
State correctly hoisted: Partially
|
|
235
|
+
→ `isExpanded` in PostItem is local UI state — this is correct. However, if expanded
|
|
236
|
+
state needs to survive scrolling (item leaving/entering the viewport), it should be
|
|
237
|
+
hoisted to the ViewModel and keyed by post ID.
|
|
238
|
+
|
|
239
|
+
SIDE EFFECTS
|
|
240
|
+
------------
|
|
241
|
+
Side effects correctly placed: Yes
|
|
242
|
+
→ No misplaced side effects detected. ViewModel interaction is correctly in the ViewModel.
|
|
243
|
+
|
|
244
|
+
OVERALL VERDICT: NEEDS WORK
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Notes
|
|
250
|
+
|
|
251
|
+
- Run alongside Agent 01 (Android Code Reviewer) for full coverage: this agent handles Compose internals, Agent 01 handles architecture, coroutines, and Kotlin idioms.
|
|
252
|
+
- Compose compiler metrics (`-P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=...`) can confirm whether types are stable — reference the metrics when available.
|
|
253
|
+
- Tested with: Claude Sonnet 4.6, GPT-4o.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Example Input — Android Compose UI Reviewer
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
COMPOSE_VERSION: 1.7
|
|
7
|
+
KOTLIN_VERSION: 2.0
|
|
8
|
+
FILE_PATH: ui/feed/FeedScreen.kt
|
|
9
|
+
CODE:
|
|
10
|
+
@Composable
|
|
11
|
+
fun FeedScreen(viewModel: FeedViewModel = hiltViewModel()) {
|
|
12
|
+
val posts by viewModel.posts.collectAsStateWithLifecycle()
|
|
13
|
+
val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
|
|
14
|
+
|
|
15
|
+
Column {
|
|
16
|
+
if (isLoading) {
|
|
17
|
+
CircularProgressIndicator()
|
|
18
|
+
}
|
|
19
|
+
LazyColumn {
|
|
20
|
+
items(posts) { post ->
|
|
21
|
+
val formattedDate = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
|
22
|
+
.format(post.createdAt)
|
|
23
|
+
PostItem(post = post, formattedDate = formattedDate)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Composable
|
|
30
|
+
fun PostItem(post: Post, formattedDate: String) {
|
|
31
|
+
var isExpanded by remember { mutableStateOf(false) }
|
|
32
|
+
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
|
|
33
|
+
Text(post.title)
|
|
34
|
+
if (isExpanded) Text(post.body)
|
|
35
|
+
Text(formattedDate)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## What to Expect
|
|
43
|
+
|
|
44
|
+
The agent identifies 3 issues. See [`example-output.md`](example-output.md) for the full report.
|
|
45
|
+
|
|
46
|
+
**Issue map:**
|
|
47
|
+
1. `SimpleDateFormat` instantiated per item per recomposition inside `items { }` — CRITICAL
|
|
48
|
+
2. `LazyColumn items` missing `key` parameter — WARNING
|
|
49
|
+
3. `Column` wrapping `LazyColumn` — should be merged into one `LazyColumn` — INFO
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Variations
|
|
54
|
+
|
|
55
|
+
### Missing derivedStateOf
|
|
56
|
+
```
|
|
57
|
+
COMPOSE_VERSION: 1.7
|
|
58
|
+
KOTLIN_VERSION: 2.0
|
|
59
|
+
FILE_PATH: ui/cart/CartScreen.kt
|
|
60
|
+
CODE:
|
|
61
|
+
@Composable
|
|
62
|
+
fun CartScreen(items: List<CartItem>) {
|
|
63
|
+
val total = items.sumOf { it.price * it.quantity } // recalculated every recomposition
|
|
64
|
+
val isCheckoutEnabled = total > 0 && items.isNotEmpty()
|
|
65
|
+
Text("Total: $$total")
|
|
66
|
+
Button(enabled = isCheckoutEnabled, onClick = {}) { Text("Checkout") }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
Issue: `total` and `isCheckoutEnabled` should be `remember(items) { ... }` or `derivedStateOf { ... }`.
|
|
70
|
+
|
|
71
|
+
### Incorrect LaunchedEffect key
|
|
72
|
+
```
|
|
73
|
+
COMPOSE_VERSION: 1.7
|
|
74
|
+
KOTLIN_VERSION: 2.0
|
|
75
|
+
FILE_PATH: ui/profile/ProfileScreen.kt
|
|
76
|
+
CODE:
|
|
77
|
+
@Composable
|
|
78
|
+
fun ProfileScreen(userId: String, viewModel: ProfileViewModel = hiltViewModel()) {
|
|
79
|
+
LaunchedEffect(Unit) { // wrong key — won't restart when userId changes
|
|
80
|
+
viewModel.loadProfile(userId)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
Issue: Key should be `LaunchedEffect(userId)` so the effect re-fires when the `userId` changes (e.g., viewing different profiles).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Example Output — Android Compose UI Reviewer
|
|
2
|
+
|
|
3
|
+
Full agent response to the input in [`example-input.md`](example-input.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
COMPOSE REVIEW
|
|
9
|
+
==============
|
|
10
|
+
File: ui/feed/FeedScreen.kt
|
|
11
|
+
Composable(s): FeedScreen, PostItem
|
|
12
|
+
Issues Found: 3
|
|
13
|
+
Critical: 1 Warning: 1 Info: 1
|
|
14
|
+
|
|
15
|
+
FINDINGS
|
|
16
|
+
--------
|
|
17
|
+
|
|
18
|
+
[CRITICAL] FeedScreen — SimpleDateFormat instantiated inside LazyColumn items lambda
|
|
19
|
+
Problem : SimpleDateFormat("dd MMM yyyy", Locale.getDefault()) is constructed fresh
|
|
20
|
+
for EVERY post on EVERY recomposition of FeedScreen. SimpleDateFormat construction
|
|
21
|
+
is expensive — it involves locale lookup, pattern parsing, and calendar initialization.
|
|
22
|
+
With 20 visible posts and recompositions triggered by any parent state change
|
|
23
|
+
(e.g., isLoading toggling), this creates hundreds of object allocations per second
|
|
24
|
+
during active use and significantly increases GC pressure.
|
|
25
|
+
Fix :
|
|
26
|
+
Option A — memoize per post inside the items lambda:
|
|
27
|
+
items(posts, key = { it.id }) { post ->
|
|
28
|
+
val formattedDate = remember(post.createdAt) {
|
|
29
|
+
SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(post.createdAt)
|
|
30
|
+
}
|
|
31
|
+
PostItem(post = post, formattedDate = formattedDate)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Option B (preferred) — format in the ViewModel or mapper, expose as String:
|
|
35
|
+
// In ViewModel or UiModel mapper:
|
|
36
|
+
data class PostUiModel(val id: String, val title: String, val body: String, val date: String)
|
|
37
|
+
// date = SimpleDateFormat(...).format(createdAt) — runs once, not on every recomposition
|
|
38
|
+
|
|
39
|
+
Option C — use Kotlin stdlib format which avoids SimpleDateFormat entirely:
|
|
40
|
+
val formattedDate = remember(post.createdAt) {
|
|
41
|
+
post.createdAt.toInstant().atZone(ZoneId.systemDefault())
|
|
42
|
+
.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[WARNING] FeedScreen — LazyColumn items missing key parameter
|
|
46
|
+
Problem : Without a key, Compose cannot identify which item is which when the list
|
|
47
|
+
changes. On any insertion, deletion, or reorder:
|
|
48
|
+
- All visible items recompose unnecessarily
|
|
49
|
+
- PostItem loses its isExpanded local state (expand/collapse resets)
|
|
50
|
+
- Animations (if added later) will not animate correctly
|
|
51
|
+
For 50+ posts this causes visible jank on data refresh.
|
|
52
|
+
Fix :
|
|
53
|
+
items(
|
|
54
|
+
items = posts,
|
|
55
|
+
key = { it.id }, // stable String ID — item identity across recompositions
|
|
56
|
+
contentType = { "post_item" } // allows Compose to reuse composition nodes efficiently
|
|
57
|
+
) { post -> ... }
|
|
58
|
+
|
|
59
|
+
[INFO] FeedScreen — Column wrapping LazyColumn limits scroll composition
|
|
60
|
+
Problem : Wrapping LazyColumn inside Column is fine here but creates a fragile structure.
|
|
61
|
+
If a scrollable header or footer is added above/below the Column, the Column
|
|
62
|
+
forces LazyColumn to measure all items eagerly (no lazy virtualization). This is
|
|
63
|
+
a common source of "ConstraintsException: Nesting scrollable in the same direction"
|
|
64
|
+
errors.
|
|
65
|
+
Fix : Use a single LazyColumn with header and loading items:
|
|
66
|
+
LazyColumn {
|
|
67
|
+
if (isLoading) {
|
|
68
|
+
item(key = "loading") { CircularProgressIndicator() }
|
|
69
|
+
}
|
|
70
|
+
items(posts, key = { it.id }, contentType = { "post" }) { post ->
|
|
71
|
+
val formattedDate = remember(post.createdAt) { ... }
|
|
72
|
+
PostItem(post = post, formattedDate = formattedDate)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
RECOMPOSITION SCOPE ASSESSMENT
|
|
77
|
+
-------------------------------
|
|
78
|
+
Recomposition scopes identified: 2 (FeedScreen body, PostItem body)
|
|
79
|
+
Unnecessary broad scopes: Yes
|
|
80
|
+
→ Both `posts` and `isLoading` are read in FeedScreen body. A change to `isLoading`
|
|
81
|
+
while `posts` is stable causes the entire Column (including LazyColumn) to recompose.
|
|
82
|
+
Extract the loading indicator into a separate composable that reads `isLoading`
|
|
83
|
+
independently, or use an AnimatedVisibility that reads isLoading in its own scope:
|
|
84
|
+
AnimatedVisibility(visible = isLoading) { CircularProgressIndicator() }
|
|
85
|
+
With AnimatedVisibility, the visibility animation runs without recomposing the LazyColumn.
|
|
86
|
+
|
|
87
|
+
STATE HOISTING
|
|
88
|
+
--------------
|
|
89
|
+
State correctly hoisted: Partially
|
|
90
|
+
→ `isExpanded` in PostItem is local UI state — this is correct for toggle state.
|
|
91
|
+
However, if expanded state must survive scrolling (PostItem leaving the viewport
|
|
92
|
+
causes it to recompose and lose state), hoist `isExpanded` to the ViewModel keyed
|
|
93
|
+
by post ID, or use rememberSaveable with a custom Saver.
|
|
94
|
+
|
|
95
|
+
SIDE EFFECTS
|
|
96
|
+
------------
|
|
97
|
+
Side effects correctly placed: Yes
|
|
98
|
+
→ No misplaced side effects detected. ViewModel is called from button handlers,
|
|
99
|
+
not directly from the Composable body.
|
|
100
|
+
|
|
101
|
+
OVERALL VERDICT: NEEDS WORK
|
|
102
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Android Crash Analyzer (Legacy)
|
|
2
|
+
|
|
3
|
+
> **This agent has been superseded by the enhanced [`android-crash-analyzer`](../android-crash-analyzer/).**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What Changed
|
|
8
|
+
|
|
9
|
+
The original crash analyzer at `agents/android/crash-analyzer/` used a 6-section output format and covered Android and iOS in a single agent.
|
|
10
|
+
|
|
11
|
+
It has been replaced by two specialized agents:
|
|
12
|
+
|
|
13
|
+
| New Agent | Platform | What's Better |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| [`android/android-crash-analyzer`](../android-crash-analyzer/) | Android | 9-section output, ANR support, LeakCanary, RecyclerView, Firebase Crashlytics, coroutine-specific rules |
|
|
16
|
+
| [`ios/crash-analyzer`](../../ios/crash-analyzer/) | iOS | ARC, force-unwrap, EXC_BAD_ACCESS, watchdog terminations, SwiftUI crashes |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Migrate in 30 Seconds
|
|
21
|
+
|
|
22
|
+
**Before:**
|
|
23
|
+
```
|
|
24
|
+
# Using the old agent
|
|
25
|
+
Use agents/android/crash-analyzer/agent.md system prompt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**After:**
|
|
29
|
+
```
|
|
30
|
+
# Use the enhanced Android agent
|
|
31
|
+
Use agents/android/android-crash-analyzer/agent.md system prompt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The new input format is identical — just add the `USER_ACTION` field for significantly better lifecycle crash analysis:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
PLATFORM: Android
|
|
38
|
+
APP_VERSION: <version>
|
|
39
|
+
OS_VERSION: <Android version>
|
|
40
|
+
DEVICE: <device model>
|
|
41
|
+
USER_ACTION: <what the user was doing> ← new, highly recommended
|
|
42
|
+
CRASH_LOG:
|
|
43
|
+
<paste the full crash log>
|
|
44
|
+
RELATED_CODE: <optional>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Why This File Exists
|
|
50
|
+
|
|
51
|
+
The `agents/android/crash-analyzer/` folder is kept to avoid breaking existing links and bookmarks. The `agent.md` inside still works, but the enhanced version produces significantly better output.
|
|
52
|
+
|
|
53
|
+
**Recommendation:** Update any team documentation, Cursor rules, or chat session bookmarks to point to `agents/android/android-crash-analyzer/`.
|