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,568 @@
|
|
|
1
|
+
# Example Output — Compose Screen Builder
|
|
2
|
+
|
|
3
|
+
Generated from [`example-input.md`](example-input.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## File: OrderHistoryUiState.kt
|
|
8
|
+
|
|
9
|
+
```kotlin
|
|
10
|
+
package com.example.app.ui.orders.history
|
|
11
|
+
|
|
12
|
+
// One-shot events that should not be replayed on recomposition.
|
|
13
|
+
sealed class OrderHistoryUiEvent {
|
|
14
|
+
data class NavigateToDetail(val orderId: String) : OrderHistoryUiEvent()
|
|
15
|
+
object NavigateToHome : OrderHistoryUiEvent()
|
|
16
|
+
object NavigateUp : OrderHistoryUiEvent()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Single source of truth for the screen's visual state.
|
|
20
|
+
sealed class OrderHistoryUiState {
|
|
21
|
+
object Loading : OrderHistoryUiState()
|
|
22
|
+
object Empty : OrderHistoryUiState()
|
|
23
|
+
data class Success(
|
|
24
|
+
val orders: List<OrderUiModel>,
|
|
25
|
+
val isRefreshing: Boolean = false
|
|
26
|
+
) : OrderHistoryUiState()
|
|
27
|
+
data class Error(val message: String) : OrderHistoryUiState()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
data class OrderUiModel(
|
|
31
|
+
val orderId: String,
|
|
32
|
+
val displayId: String, // e.g. "#ORD-4821"
|
|
33
|
+
val status: OrderStatus,
|
|
34
|
+
val itemCount: Int,
|
|
35
|
+
val totalPrice: String, // pre-formatted, e.g. "$48.50"
|
|
36
|
+
val orderDate: String // pre-formatted, e.g. "May 12, 2025"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
enum class OrderStatus { DELIVERED, PROCESSING, CANCELLED }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## File: OrderHistoryViewModel.kt
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
package com.example.app.ui.orders.history
|
|
48
|
+
|
|
49
|
+
import androidx.lifecycle.SavedStateHandle
|
|
50
|
+
import androidx.lifecycle.ViewModel
|
|
51
|
+
import androidx.lifecycle.viewModelScope
|
|
52
|
+
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
53
|
+
import kotlinx.coroutines.CancellationException
|
|
54
|
+
import kotlinx.coroutines.channels.Channel
|
|
55
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
56
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
57
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
58
|
+
import kotlinx.coroutines.flow.receiveAsFlow
|
|
59
|
+
import kotlinx.coroutines.launch
|
|
60
|
+
import javax.inject.Inject
|
|
61
|
+
|
|
62
|
+
@HiltViewModel
|
|
63
|
+
class OrderHistoryViewModel @Inject constructor(
|
|
64
|
+
savedStateHandle: SavedStateHandle,
|
|
65
|
+
private val getOrderHistory: GetOrderHistoryUseCase
|
|
66
|
+
) : ViewModel() {
|
|
67
|
+
|
|
68
|
+
// Pulled from navigation args — NavController puts them in SavedStateHandle automatically.
|
|
69
|
+
private val userId: String = checkNotNull(savedStateHandle["userId"])
|
|
70
|
+
|
|
71
|
+
private val _uiState = MutableStateFlow<OrderHistoryUiState>(OrderHistoryUiState.Loading)
|
|
72
|
+
val uiState: StateFlow<OrderHistoryUiState> = _uiState.asStateFlow()
|
|
73
|
+
|
|
74
|
+
// Channel for one-shot events — SharedFlow would re-emit on new collectors (config change).
|
|
75
|
+
// Channel with BUFFERED capacity holds the event until the screen collects it.
|
|
76
|
+
private val _uiEvents = Channel<OrderHistoryUiEvent>(Channel.BUFFERED)
|
|
77
|
+
val uiEvents = _uiEvents.receiveAsFlow()
|
|
78
|
+
|
|
79
|
+
init {
|
|
80
|
+
loadOrders()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun loadOrders() {
|
|
84
|
+
viewModelScope.launch {
|
|
85
|
+
_uiState.value = OrderHistoryUiState.Loading
|
|
86
|
+
try {
|
|
87
|
+
val orders = getOrderHistory(userId)
|
|
88
|
+
_uiState.value = if (orders.isEmpty()) {
|
|
89
|
+
OrderHistoryUiState.Empty
|
|
90
|
+
} else {
|
|
91
|
+
OrderHistoryUiState.Success(orders = orders.map { it.toUiModel() })
|
|
92
|
+
}
|
|
93
|
+
} catch (e: CancellationException) {
|
|
94
|
+
throw e // Never swallow CancellationException
|
|
95
|
+
} catch (e: Exception) {
|
|
96
|
+
_uiState.value = OrderHistoryUiState.Error(
|
|
97
|
+
message = e.message ?: "Failed to load orders"
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun refresh() {
|
|
104
|
+
val current = _uiState.value
|
|
105
|
+
if (current is OrderHistoryUiState.Success) {
|
|
106
|
+
// Mark as refreshing without losing the existing list.
|
|
107
|
+
_uiState.value = current.copy(isRefreshing = true)
|
|
108
|
+
}
|
|
109
|
+
viewModelScope.launch {
|
|
110
|
+
try {
|
|
111
|
+
val orders = getOrderHistory(userId)
|
|
112
|
+
_uiState.value = if (orders.isEmpty()) {
|
|
113
|
+
OrderHistoryUiState.Empty
|
|
114
|
+
} else {
|
|
115
|
+
OrderHistoryUiState.Success(orders = orders.map { it.toUiModel() })
|
|
116
|
+
}
|
|
117
|
+
} catch (e: CancellationException) {
|
|
118
|
+
throw e
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
// On refresh failure, keep the existing list but stop the spinner.
|
|
121
|
+
if (_uiState.value is OrderHistoryUiState.Success) {
|
|
122
|
+
_uiState.value = (current as OrderHistoryUiState.Success).copy(isRefreshing = false)
|
|
123
|
+
} else {
|
|
124
|
+
_uiState.value = OrderHistoryUiState.Error(e.message ?: "Refresh failed")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fun onOrderTapped(orderId: String) {
|
|
131
|
+
viewModelScope.launch {
|
|
132
|
+
_uiEvents.send(OrderHistoryUiEvent.NavigateToDetail(orderId))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun onStartShoppingTapped() {
|
|
137
|
+
viewModelScope.launch {
|
|
138
|
+
_uiEvents.send(OrderHistoryUiEvent.NavigateToHome)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fun onNavigateUp() {
|
|
143
|
+
viewModelScope.launch {
|
|
144
|
+
_uiEvents.send(OrderHistoryUiEvent.NavigateUp)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Domain stub (place in domain module, zero Android imports) ---
|
|
150
|
+
|
|
151
|
+
class GetOrderHistoryUseCase @Inject constructor(
|
|
152
|
+
private val repository: OrderRepository
|
|
153
|
+
) {
|
|
154
|
+
suspend operator fun invoke(userId: String): List<Order> =
|
|
155
|
+
repository.getOrderHistory(userId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Mapper (place in UI or data-ui mapping layer) ---
|
|
159
|
+
|
|
160
|
+
fun Order.toUiModel() = OrderUiModel(
|
|
161
|
+
orderId = id,
|
|
162
|
+
displayId = "#ORD-${id.takeLast(4).uppercase()}",
|
|
163
|
+
status = when (status) {
|
|
164
|
+
"DELIVERED" -> OrderStatus.DELIVERED
|
|
165
|
+
"PROCESSING" -> OrderStatus.PROCESSING
|
|
166
|
+
"CANCELLED" -> OrderStatus.CANCELLED
|
|
167
|
+
else -> OrderStatus.PROCESSING
|
|
168
|
+
},
|
|
169
|
+
itemCount = items.size,
|
|
170
|
+
totalPrice = "$${String.format("%.2f", totalPrice)}",
|
|
171
|
+
orderDate = date.toDisplayString() // your date formatting extension
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## File: OrderHistoryScreen.kt
|
|
178
|
+
|
|
179
|
+
```kotlin
|
|
180
|
+
package com.example.app.ui.orders.history
|
|
181
|
+
|
|
182
|
+
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
183
|
+
import androidx.compose.foundation.clickable
|
|
184
|
+
import androidx.compose.foundation.layout.*
|
|
185
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
186
|
+
import androidx.compose.foundation.lazy.items
|
|
187
|
+
import androidx.compose.material.icons.Icons
|
|
188
|
+
import androidx.compose.material.icons.rounded.ArrowBack
|
|
189
|
+
import androidx.compose.material.icons.rounded.ShoppingBag
|
|
190
|
+
import androidx.compose.material3.*
|
|
191
|
+
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
192
|
+
import androidx.compose.runtime.*
|
|
193
|
+
import androidx.compose.ui.Alignment
|
|
194
|
+
import androidx.compose.ui.Modifier
|
|
195
|
+
import androidx.compose.ui.graphics.Color
|
|
196
|
+
import androidx.compose.ui.semantics.contentDescription
|
|
197
|
+
import androidx.compose.ui.semantics.semantics
|
|
198
|
+
import androidx.compose.ui.tooling.preview.Preview
|
|
199
|
+
import androidx.compose.ui.unit.dp
|
|
200
|
+
import androidx.hilt.navigation.compose.hiltViewModel
|
|
201
|
+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
202
|
+
import androidx.navigation.NavController
|
|
203
|
+
import androidx.navigation.compose.rememberNavController
|
|
204
|
+
|
|
205
|
+
// ─── Route definition ────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
object OrderHistoryRoute {
|
|
208
|
+
const val ROUTE = "order_history/{userId}"
|
|
209
|
+
fun build(userId: String) = "order_history/$userId"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Screen entry point ──────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
@Composable
|
|
215
|
+
fun OrderHistoryScreen(
|
|
216
|
+
navController: NavController,
|
|
217
|
+
viewModel: OrderHistoryViewModel = hiltViewModel()
|
|
218
|
+
) {
|
|
219
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
220
|
+
|
|
221
|
+
// One-shot event handling — collected once per composition lifetime.
|
|
222
|
+
LaunchedEffect(Unit) {
|
|
223
|
+
viewModel.uiEvents.collect { event ->
|
|
224
|
+
when (event) {
|
|
225
|
+
is OrderHistoryUiEvent.NavigateToDetail ->
|
|
226
|
+
navController.navigate("order_detail/${event.orderId}")
|
|
227
|
+
OrderHistoryUiEvent.NavigateToHome ->
|
|
228
|
+
navController.navigate("home") { popUpTo("home") { inclusive = false } }
|
|
229
|
+
OrderHistoryUiEvent.NavigateUp ->
|
|
230
|
+
navController.navigateUp()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
OrderHistoryContent(
|
|
236
|
+
uiState = uiState,
|
|
237
|
+
onRefresh = viewModel::refresh,
|
|
238
|
+
onOrderTapped = viewModel::onOrderTapped,
|
|
239
|
+
onStartShopping = viewModel::onStartShoppingTapped,
|
|
240
|
+
onRetry = viewModel::loadOrders,
|
|
241
|
+
onNavigateUp = viewModel::onNavigateUp
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Stateless content (testable in isolation) ───────────────────────────────
|
|
246
|
+
|
|
247
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
248
|
+
@Composable
|
|
249
|
+
fun OrderHistoryContent(
|
|
250
|
+
uiState: OrderHistoryUiState,
|
|
251
|
+
onRefresh: () -> Unit,
|
|
252
|
+
onOrderTapped: (String) -> Unit,
|
|
253
|
+
onStartShopping: () -> Unit,
|
|
254
|
+
onRetry: () -> Unit,
|
|
255
|
+
onNavigateUp: () -> Unit
|
|
256
|
+
) {
|
|
257
|
+
Scaffold(
|
|
258
|
+
topBar = {
|
|
259
|
+
CenterAlignedTopAppBar(
|
|
260
|
+
title = { Text("My Orders") },
|
|
261
|
+
navigationIcon = {
|
|
262
|
+
IconButton(
|
|
263
|
+
onClick = onNavigateUp,
|
|
264
|
+
modifier = Modifier.minimumInteractiveComponentSize()
|
|
265
|
+
) {
|
|
266
|
+
Icon(
|
|
267
|
+
imageVector = Icons.Rounded.ArrowBack,
|
|
268
|
+
contentDescription = "Go back"
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
) { innerPadding ->
|
|
275
|
+
when (uiState) {
|
|
276
|
+
OrderHistoryUiState.Loading -> OrderHistoryLoading(innerPadding)
|
|
277
|
+
|
|
278
|
+
OrderHistoryUiState.Empty -> OrderHistoryEmpty(
|
|
279
|
+
modifier = Modifier.padding(innerPadding),
|
|
280
|
+
onStartShopping = onStartShopping
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
is OrderHistoryUiState.Error -> OrderHistoryError(
|
|
284
|
+
message = uiState.message,
|
|
285
|
+
onRetry = onRetry,
|
|
286
|
+
modifier = Modifier.padding(innerPadding)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
is OrderHistoryUiState.Success -> OrderHistorySuccess(
|
|
290
|
+
orders = uiState.orders,
|
|
291
|
+
isRefreshing = uiState.isRefreshing,
|
|
292
|
+
onRefresh = onRefresh,
|
|
293
|
+
onOrderTapped = onOrderTapped,
|
|
294
|
+
modifier = Modifier.padding(innerPadding)
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── State renderers ─────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
@Composable
|
|
303
|
+
private fun OrderHistoryLoading(paddingValues: PaddingValues) {
|
|
304
|
+
Box(
|
|
305
|
+
modifier = Modifier
|
|
306
|
+
.fillMaxSize()
|
|
307
|
+
.padding(paddingValues),
|
|
308
|
+
contentAlignment = Alignment.Center
|
|
309
|
+
) {
|
|
310
|
+
CircularProgressIndicator(
|
|
311
|
+
modifier = Modifier.semantics { contentDescription = "Loading orders" }
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@Composable
|
|
317
|
+
private fun OrderHistoryEmpty(
|
|
318
|
+
onStartShopping: () -> Unit,
|
|
319
|
+
modifier: Modifier = Modifier
|
|
320
|
+
) {
|
|
321
|
+
Column(
|
|
322
|
+
modifier = modifier.fillMaxSize(),
|
|
323
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
324
|
+
verticalArrangement = Arrangement.Center
|
|
325
|
+
) {
|
|
326
|
+
Icon(
|
|
327
|
+
imageVector = Icons.Rounded.ShoppingBag,
|
|
328
|
+
contentDescription = null, // decorative — message text provides context
|
|
329
|
+
modifier = Modifier.size(72.dp),
|
|
330
|
+
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
331
|
+
)
|
|
332
|
+
Spacer(Modifier.height(16.dp))
|
|
333
|
+
Text(
|
|
334
|
+
text = "No orders yet",
|
|
335
|
+
style = MaterialTheme.typography.titleMedium,
|
|
336
|
+
color = MaterialTheme.colorScheme.onSurface
|
|
337
|
+
)
|
|
338
|
+
Spacer(Modifier.height(8.dp))
|
|
339
|
+
Text(
|
|
340
|
+
text = "Your completed orders will appear here.",
|
|
341
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
342
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
343
|
+
)
|
|
344
|
+
Spacer(Modifier.height(24.dp))
|
|
345
|
+
FilledTonalButton(onClick = onStartShopping) {
|
|
346
|
+
Text("Start Shopping")
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@Composable
|
|
352
|
+
private fun OrderHistoryError(
|
|
353
|
+
message: String,
|
|
354
|
+
onRetry: () -> Unit,
|
|
355
|
+
modifier: Modifier = Modifier
|
|
356
|
+
) {
|
|
357
|
+
Column(
|
|
358
|
+
modifier = modifier.fillMaxSize(),
|
|
359
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
360
|
+
verticalArrangement = Arrangement.Center
|
|
361
|
+
) {
|
|
362
|
+
Text(
|
|
363
|
+
text = "Something went wrong",
|
|
364
|
+
style = MaterialTheme.typography.titleMedium
|
|
365
|
+
)
|
|
366
|
+
Spacer(Modifier.height(8.dp))
|
|
367
|
+
Text(
|
|
368
|
+
text = message,
|
|
369
|
+
style = MaterialTheme.typography.bodySmall,
|
|
370
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
371
|
+
)
|
|
372
|
+
Spacer(Modifier.height(24.dp))
|
|
373
|
+
Button(onClick = onRetry) { Text("Retry") }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
378
|
+
@Composable
|
|
379
|
+
private fun OrderHistorySuccess(
|
|
380
|
+
orders: List<OrderUiModel>,
|
|
381
|
+
isRefreshing: Boolean,
|
|
382
|
+
onRefresh: () -> Unit,
|
|
383
|
+
onOrderTapped: (String) -> Unit,
|
|
384
|
+
modifier: Modifier = Modifier
|
|
385
|
+
) {
|
|
386
|
+
PullToRefreshBox(
|
|
387
|
+
isRefreshing = isRefreshing,
|
|
388
|
+
onRefresh = onRefresh,
|
|
389
|
+
modifier = modifier.fillMaxSize()
|
|
390
|
+
) {
|
|
391
|
+
LazyColumn(
|
|
392
|
+
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
|
|
393
|
+
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
394
|
+
) {
|
|
395
|
+
items(
|
|
396
|
+
items = orders,
|
|
397
|
+
key = { it.orderId }, // stable key prevents unnecessary recomposition
|
|
398
|
+
contentType = { "order_card" } // allows Compose to reuse composition nodes
|
|
399
|
+
) { order ->
|
|
400
|
+
OrderCard(order = order, onClick = { onOrderTapped(order.orderId) })
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Order card component ────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
@Composable
|
|
409
|
+
private fun OrderCard(
|
|
410
|
+
order: OrderUiModel,
|
|
411
|
+
onClick: () -> Unit,
|
|
412
|
+
modifier: Modifier = Modifier
|
|
413
|
+
) {
|
|
414
|
+
val statusColor = when (order.status) {
|
|
415
|
+
OrderStatus.DELIVERED -> Color(0xFF2E7D32) // exception: semantic color not in M3 scheme
|
|
416
|
+
OrderStatus.PROCESSING -> Color(0xFFE65100)
|
|
417
|
+
OrderStatus.CANCELLED -> MaterialTheme.colorScheme.error
|
|
418
|
+
}
|
|
419
|
+
val statusLabel = when (order.status) {
|
|
420
|
+
OrderStatus.DELIVERED -> "Delivered"
|
|
421
|
+
OrderStatus.PROCESSING -> "Processing"
|
|
422
|
+
OrderStatus.CANCELLED -> "Cancelled"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
Card(
|
|
426
|
+
modifier = modifier
|
|
427
|
+
.fillMaxWidth()
|
|
428
|
+
.clickable(
|
|
429
|
+
onClickLabel = "View order ${order.displayId}",
|
|
430
|
+
onClick = onClick
|
|
431
|
+
),
|
|
432
|
+
colors = CardDefaults.cardColors(
|
|
433
|
+
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
|
434
|
+
)
|
|
435
|
+
) {
|
|
436
|
+
Column(modifier = Modifier.padding(16.dp)) {
|
|
437
|
+
Row(
|
|
438
|
+
modifier = Modifier.fillMaxWidth(),
|
|
439
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
440
|
+
verticalAlignment = Alignment.CenterVertically
|
|
441
|
+
) {
|
|
442
|
+
Text(
|
|
443
|
+
text = order.displayId,
|
|
444
|
+
style = MaterialTheme.typography.titleSmall
|
|
445
|
+
)
|
|
446
|
+
Surface(
|
|
447
|
+
color = statusColor.copy(alpha = 0.12f),
|
|
448
|
+
shape = MaterialTheme.shapes.small
|
|
449
|
+
) {
|
|
450
|
+
Text(
|
|
451
|
+
text = statusLabel,
|
|
452
|
+
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
|
453
|
+
style = MaterialTheme.typography.labelSmall,
|
|
454
|
+
color = statusColor
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
Spacer(Modifier.height(8.dp))
|
|
459
|
+
Row(
|
|
460
|
+
modifier = Modifier.fillMaxWidth(),
|
|
461
|
+
horizontalArrangement = Arrangement.SpaceBetween
|
|
462
|
+
) {
|
|
463
|
+
Text(
|
|
464
|
+
text = "${order.itemCount} ${if (order.itemCount == 1) "item" else "items"}",
|
|
465
|
+
style = MaterialTheme.typography.bodySmall,
|
|
466
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
467
|
+
)
|
|
468
|
+
Text(
|
|
469
|
+
text = order.totalPrice,
|
|
470
|
+
style = MaterialTheme.typography.bodyMedium
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
Spacer(Modifier.height(4.dp))
|
|
474
|
+
Text(
|
|
475
|
+
text = order.orderDate,
|
|
476
|
+
style = MaterialTheme.typography.bodySmall,
|
|
477
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── Previews ────────────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
private val previewOrders = listOf(
|
|
486
|
+
OrderUiModel("1", "#ORD-4821", OrderStatus.DELIVERED, 3, "$48.50", "May 12, 2025"),
|
|
487
|
+
OrderUiModel("2", "#ORD-3917", OrderStatus.PROCESSING, 1, "$12.99", "May 10, 2025"),
|
|
488
|
+
OrderUiModel("3", "#ORD-3105", OrderStatus.CANCELLED, 2, "$29.00", "Apr 28, 2025"),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
@Preview(showBackground = true, name = "Light — Success")
|
|
492
|
+
@Composable
|
|
493
|
+
private fun OrderHistorySuccessPreview() {
|
|
494
|
+
MaterialTheme {
|
|
495
|
+
OrderHistoryContent(
|
|
496
|
+
uiState = OrderHistoryUiState.Success(orders = previewOrders),
|
|
497
|
+
onRefresh = {}, onOrderTapped = {}, onStartShopping = {}, onRetry = {}, onNavigateUp = {}
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES, name = "Dark — Success")
|
|
503
|
+
@Composable
|
|
504
|
+
private fun OrderHistorySuccessDarkPreview() {
|
|
505
|
+
MaterialTheme {
|
|
506
|
+
OrderHistoryContent(
|
|
507
|
+
uiState = OrderHistoryUiState.Success(orders = previewOrders),
|
|
508
|
+
onRefresh = {}, onOrderTapped = {}, onStartShopping = {}, onRetry = {}, onNavigateUp = {}
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
@Preview(showBackground = true, name = "Empty")
|
|
514
|
+
@Composable
|
|
515
|
+
private fun OrderHistoryEmptyPreview() {
|
|
516
|
+
MaterialTheme {
|
|
517
|
+
OrderHistoryContent(
|
|
518
|
+
uiState = OrderHistoryUiState.Empty,
|
|
519
|
+
onRefresh = {}, onOrderTapped = {}, onStartShopping = {}, onRetry = {}, onNavigateUp = {}
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Preview(showBackground = true, name = "Error")
|
|
525
|
+
@Composable
|
|
526
|
+
private fun OrderHistoryErrorPreview() {
|
|
527
|
+
MaterialTheme {
|
|
528
|
+
OrderHistoryContent(
|
|
529
|
+
uiState = OrderHistoryUiState.Error("Unable to connect to server"),
|
|
530
|
+
onRefresh = {}, onOrderTapped = {}, onStartShopping = {}, onRetry = {}, onNavigateUp = {}
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Navigation Registration
|
|
539
|
+
|
|
540
|
+
Add this block to your `NavHost` in your app's main `NavGraph.kt`:
|
|
541
|
+
|
|
542
|
+
```kotlin
|
|
543
|
+
composable(
|
|
544
|
+
route = OrderHistoryRoute.ROUTE,
|
|
545
|
+
arguments = listOf(navArgument("userId") { type = NavType.StringType })
|
|
546
|
+
) {
|
|
547
|
+
OrderHistoryScreen(navController = navController)
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Navigate to this screen from anywhere:
|
|
552
|
+
|
|
553
|
+
```kotlin
|
|
554
|
+
navController.navigate(OrderHistoryRoute.build(userId = currentUserId))
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Gradle Dependencies
|
|
560
|
+
|
|
561
|
+
```kotlin
|
|
562
|
+
// All required for this screen — add to app/build.gradle.kts if not already present:
|
|
563
|
+
implementation("androidx.compose.material3:material3:1.3.1")
|
|
564
|
+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
|
565
|
+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") // collectAsStateWithLifecycle
|
|
566
|
+
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
|
567
|
+
implementation("androidx.navigation:navigation-compose:2.8.5")
|
|
568
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Android Compose UI Reviewer Agent
|
|
2
|
+
|
|
3
|
+
> A focused Compose-only reviewer. Paste a `@Composable` function and get a recomposition scope analysis, state hoisting assessment, `remember`/`derivedStateOf` correctness check, and `LazyColumn` performance audit.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What This Agent Does
|
|
8
|
+
|
|
9
|
+
Specialized for Compose-specific issues that the general Android Code Reviewer misses. Analyzes:
|
|
10
|
+
|
|
11
|
+
- **Recomposition scope** — is state read inside the smallest possible lambda to minimize rebuild cost?
|
|
12
|
+
- **`remember` vs `rememberSaveable` vs `derivedStateOf`** — is the right tool being used? Are `remember` keys correct?
|
|
13
|
+
- **State hoisting** — is mutable state owned at the right level? Is there split-brain state?
|
|
14
|
+
- **`LazyColumn` / `LazyRow`** — `key`, `contentType`, expensive operations inside `items { }`, `Column { items.forEach }` anti-pattern
|
|
15
|
+
- **Side effects** — `LaunchedEffect`, `SideEffect`, `DisposableEffect` placement and key correctness
|
|
16
|
+
- **Slot API design** — is complex child content passed as a `@Composable () -> Unit` lambda?
|
|
17
|
+
- **Stability** — are unstable types (`List`, `Map`, regular classes) causing unnecessary recompositions?
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to Use This Instead of the General Code Reviewer
|
|
22
|
+
|
|
23
|
+
| Situation | Agent to Use |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Full file review (architecture, coroutines, Compose) | `agents/android/code-reviewer` |
|
|
26
|
+
| Compose-specific performance issue (jank, recompositions) | **This agent** |
|
|
27
|
+
| LazyColumn scrolling is slow | **This agent** |
|
|
28
|
+
| Unexplained UI flicker or state loss | **This agent** |
|
|
29
|
+
| Compose compiler metrics showing unstable types | **This agent** |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Files
|
|
34
|
+
|
|
35
|
+
| File | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| [`agent.md`](agent.md) | Input format, output format, full system prompt |
|
|
38
|
+
| [`example-input.md`](example-input.md) | FeedScreen with LazyColumn and recomposition issues |
|
|
39
|
+
| [`example-output.md`](example-output.md) | Full recomposition + state hoisting report |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
COMPOSE_VERSION: 1.7
|
|
47
|
+
KOTLIN_VERSION: 2.0
|
|
48
|
+
FILE_PATH: ui/feed/FeedScreen.kt
|
|
49
|
+
CODE:
|
|
50
|
+
[paste your @Composable function]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Output Preview
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
[CRITICAL] FeedScreen — SimpleDateFormat instantiated inside LazyColumn items lambda
|
|
59
|
+
Problem : Created fresh for every post on every recomposition — expensive object allocation.
|
|
60
|
+
Fix : remember(post.createdAt) { SimpleDateFormat(...).format(post.createdAt) }
|
|
61
|
+
Or: format in the ViewModel and expose pre-formatted strings.
|
|
62
|
+
|
|
63
|
+
[WARNING] FeedScreen — LazyColumn items missing key parameter
|
|
64
|
+
Problem : Without key, any list change causes PostItem to lose its isExpanded state.
|
|
65
|
+
Fix : items(posts, key = { it.id }) { post -> ... }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Related Agents
|
|
71
|
+
|
|
72
|
+
- [`agents/android/code-reviewer`](../code-reviewer/) — full-file review including architecture and coroutines
|
|
73
|
+
- [`agents/android/compose-screen-builder`](../compose-screen-builder/) — generate Compose screens that follow these patterns from the start
|