svharness 0.8.0
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/README.md +531 -0
- package/bin/cli.js +3 -0
- package/dist/adapters/_frontmatter.js +24 -0
- package/dist/adapters/claude-code.js +12 -0
- package/dist/adapters/codechat.js +12 -0
- package/dist/adapters/cursor.js +19 -0
- package/dist/adapters/generic.js +19 -0
- package/dist/adapters/index.js +26 -0
- package/dist/adapters/qoder.js +12 -0
- package/dist/commands/apply.js +272 -0
- package/dist/commands/init.js +420 -0
- package/dist/core/agent-injector.js +192 -0
- package/dist/core/next-steps.js +91 -0
- package/dist/core/render-meta.js +81 -0
- package/dist/core/repomix-pack.js +54 -0
- package/dist/core/scaffold.js +93 -0
- package/dist/core/state.js +80 -0
- package/dist/index.js +239 -0
- package/dist/types.js +5 -0
- package/dist/utils/baseline-copy.js +591 -0
- package/dist/utils/baseline-defaults.js +106 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/validate-args.js +132 -0
- package/dist/utils/version.js +23 -0
- package/dist/wiki/abort.js +30 -0
- package/dist/wiki/config.js +79 -0
- package/dist/wiki/defaults.js +16 -0
- package/dist/wiki/envLoader.js +78 -0
- package/dist/wiki/index.js +29 -0
- package/dist/wiki/openaiCompat.js +219 -0
- package/dist/wiki/repowikiCanonicalSections.js +67 -0
- package/dist/wiki/repowikiCheckpoint.js +106 -0
- package/dist/wiki/repowikiConfig.js +9 -0
- package/dist/wiki/repowikiGit.js +73 -0
- package/dist/wiki/repowikiIndexer.js +824 -0
- package/dist/wiki/repowikiMarkdownPost.js +123 -0
- package/dist/wiki/repowikiMetadataContent.js +64 -0
- package/dist/wiki/repowikiMetadataJson.js +15 -0
- package/dist/wiki/repowikiScanner.js +156 -0
- package/dist/wiki/repowikiStructureNav.js +286 -0
- package/dist/wiki/repowikiStructureNormalize.js +218 -0
- package/dist/wiki/wikiStructureXml.js +316 -0
- package/dist/wiki/wikiTasksWriter.js +127 -0
- package/package.json +57 -0
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
- package/templates/_shared/meta/AGENTS.md.ejs +53 -0
- package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
- package/templates/_shared/meta/README.md.ejs +51 -0
- package/templates/_shared/meta/VERSION.ejs +1 -0
- package/templates/_shared/meta/harness.yaml.ejs +52 -0
- package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
- package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
- package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
- package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
- package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +168 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +183 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
- package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
- package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
- package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
- package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
- package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-no-props-drilling.md +43 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-compose-ui
|
|
3
|
+
description: 构建 Jetpack Compose UI 时的最佳实践指引,覆盖 Composable 设计、性能优化、Modifier 规范、列表/网格模式和 Material 3 主题。刚性约束由 rules 加载,本技能聚焦实现模式与配方。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jetpack Compose UI — 实现指引
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
本技能指导如何构建高性能、可复用、可测试的 Composable 组件。
|
|
11
|
+
|
|
12
|
+
> **刚性约束已由 rules 加载,本技能不再重复。** 详见:
|
|
13
|
+
> - `harness-compose-mandatory` — Modifier 位置、key、稳定类型、副作用约束
|
|
14
|
+
> - `harness-mvi-layering` — 容器层/纯 UI 层分离、Reducer 纯函数约束
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Composable 设计
|
|
19
|
+
|
|
20
|
+
### 状态提升(单向数据流)
|
|
21
|
+
|
|
22
|
+
Composable 必须无状态,状态向下流动,事件向上流动:
|
|
23
|
+
|
|
24
|
+
```kotlin
|
|
25
|
+
@Composable
|
|
26
|
+
fun NameInput(
|
|
27
|
+
name: String, // 状态向下流动
|
|
28
|
+
onNameChange: (String) -> Unit, // 事件向上流动
|
|
29
|
+
modifier: Modifier = Modifier
|
|
30
|
+
) {
|
|
31
|
+
TextField(
|
|
32
|
+
value = name,
|
|
33
|
+
onValueChange = onNameChange,
|
|
34
|
+
label = { Text("Name") },
|
|
35
|
+
modifier = modifier
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
屏幕级 Composable 从 ViewModel 获取状态并向下传递:
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
@Composable
|
|
44
|
+
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
|
|
45
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
46
|
+
UserContent(
|
|
47
|
+
uiState = uiState,
|
|
48
|
+
onNameChange = viewModel::updateName,
|
|
49
|
+
onSave = viewModel::saveUser
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 插槽 API
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
@Composable
|
|
58
|
+
fun CustomScaffold(
|
|
59
|
+
topBar: @Composable () -> Unit = {},
|
|
60
|
+
bottomBar: @Composable () -> Unit = {},
|
|
61
|
+
content: @Composable (PaddingValues) -> Unit
|
|
62
|
+
) {
|
|
63
|
+
Scaffold(topBar = topBar, bottomBar = bottomBar, content = content)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
更多 API 设计模式见 [references/composable-api-guide.md](references/composable-api-guide.md)。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 性能优化
|
|
72
|
+
|
|
73
|
+
### 高效重组
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
// 列表项使用 key
|
|
77
|
+
@Composable
|
|
78
|
+
fun UserList(users: List<User>) {
|
|
79
|
+
LazyColumn {
|
|
80
|
+
items(items = users, key = { it.id }) { user ->
|
|
81
|
+
UserItem(user)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// derivedStateOf 避免不必要的重组
|
|
87
|
+
@Composable
|
|
88
|
+
fun FilteredList(items: List<Item>, query: String) {
|
|
89
|
+
val filtered by remember(items, query) {
|
|
90
|
+
derivedStateOf { items.filter { it.name.contains(query, ignoreCase = true) } }
|
|
91
|
+
}
|
|
92
|
+
LazyColumn { items(filtered) { item -> ItemRow(item) } }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Lambda 稳定性:优先方法引用
|
|
96
|
+
@Composable
|
|
97
|
+
fun GoodScreen(viewModel: MyViewModel) {
|
|
98
|
+
MyButton(onClick = viewModel::doAction) // 稳定引用
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
更多性能配方见 [references/performance-recipes.md](references/performance-recipes.md)。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 列表与网格
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
// 自适应网格
|
|
110
|
+
@Composable
|
|
111
|
+
fun ProductGrid(products: List<Product>) {
|
|
112
|
+
LazyVerticalGrid(
|
|
113
|
+
columns = GridCells.Adaptive(minSize = 160.dp),
|
|
114
|
+
contentPadding = PaddingValues(16.dp),
|
|
115
|
+
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
116
|
+
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
117
|
+
) {
|
|
118
|
+
items(products, key = { it.id }) { product -> ProductCard(product) }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 粘性标题
|
|
123
|
+
@Composable
|
|
124
|
+
fun ContactList(contacts: Map<Char, List<Contact>>) {
|
|
125
|
+
LazyColumn {
|
|
126
|
+
contacts.forEach { (initial, contactsForInitial) ->
|
|
127
|
+
stickyHeader {
|
|
128
|
+
Text(
|
|
129
|
+
text = initial.toString(),
|
|
130
|
+
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface).padding(16.dp),
|
|
131
|
+
style = MaterialTheme.typography.titleMedium
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
items(contactsForInitial, key = { it.id }) { contact -> ContactItem(contact) }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Material 3 主题
|
|
143
|
+
|
|
144
|
+
```kotlin
|
|
145
|
+
@Composable
|
|
146
|
+
fun AppTheme(
|
|
147
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
148
|
+
content: @Composable () -> Unit
|
|
149
|
+
) {
|
|
150
|
+
val colorScheme = when {
|
|
151
|
+
darkTheme -> darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80)
|
|
152
|
+
else -> lightColorScheme(primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40)
|
|
153
|
+
}
|
|
154
|
+
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 使用主题值(禁止硬编码)
|
|
158
|
+
@Composable
|
|
159
|
+
fun ThemedCard() {
|
|
160
|
+
Card(
|
|
161
|
+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
|
162
|
+
) {
|
|
163
|
+
Text(
|
|
164
|
+
text = "Themed content",
|
|
165
|
+
style = MaterialTheme.typography.bodyLarge,
|
|
166
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 反模式速查
|
|
175
|
+
|
|
176
|
+
| 反模式 | 正确做法 |
|
|
177
|
+
|--------|---------|
|
|
178
|
+
| `viewModel.loadData()` 在组合体内直接调用 | `LaunchedEffect(Unit) { viewModel.loadData() }` |
|
|
179
|
+
| `remember { mutableStateOf(initial) }` 不带 key | `remember(initial) { mutableStateOf(initial) }` |
|
|
180
|
+
| `items.filter { ... }` 直接在组合体内 | `remember(items) { derivedStateOf { items.filter { ... } } }` |
|
|
181
|
+
| `MutableList` 作为 Composable 参数 | 使用 `List` 不可变接口 |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 必须遵守的约束规则
|
|
186
|
+
|
|
187
|
+
> rules 引用路径:`../rules/<rule-name>.mdc`
|
|
188
|
+
|
|
189
|
+
- harness-compose-mandatory
|
|
190
|
+
- harness-mvi-layering
|
|
191
|
+
- harness-hilt-injection
|
|
192
|
+
- harness-coroutines-scope
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Composable API 设计指南
|
|
2
|
+
|
|
3
|
+
## 参数顺序规范
|
|
4
|
+
|
|
5
|
+
```kotlin
|
|
6
|
+
@Composable
|
|
7
|
+
fun CustomCard(
|
|
8
|
+
title: String, // 1. 必需数据参数
|
|
9
|
+
subtitle: String = "", // 2. 可选数据参数
|
|
10
|
+
modifier: Modifier = Modifier, // 3. modifier(第一个可选参数)
|
|
11
|
+
onClick: () -> Unit = {}, // 4. 事件回调
|
|
12
|
+
content: @Composable () -> Unit // 5. 插槽内容(最后)
|
|
13
|
+
) { ... }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Modifier 规范
|
|
17
|
+
|
|
18
|
+
```kotlin
|
|
19
|
+
// ✅ 正确:modifier 作为第一个可选参数
|
|
20
|
+
@Composable
|
|
21
|
+
fun FeatureCard(
|
|
22
|
+
title: String,
|
|
23
|
+
modifier: Modifier = Modifier,
|
|
24
|
+
onClick: () -> Unit = {}
|
|
25
|
+
) {
|
|
26
|
+
Card(modifier = modifier.clickable(onClick = onClick)) {
|
|
27
|
+
Text(text = title, modifier = Modifier.padding(16.dp))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ❌ 错误:modifier 不在第一个可选位置
|
|
32
|
+
@Composable
|
|
33
|
+
fun FeatureCard(
|
|
34
|
+
modifier: Modifier,
|
|
35
|
+
title: String
|
|
36
|
+
) { ... }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Modifier 应用顺序**:`padding().clickable()` 与 `clickable().padding()` 不同。通常在点击监听器之后应用影响布局的修饰符。
|
|
40
|
+
|
|
41
|
+
## 通用组件模式
|
|
42
|
+
|
|
43
|
+
### 功能卡片
|
|
44
|
+
|
|
45
|
+
```kotlin
|
|
46
|
+
@Composable
|
|
47
|
+
fun FeatureCard(
|
|
48
|
+
title: String,
|
|
49
|
+
icon: Painter,
|
|
50
|
+
onClick: () -> Unit,
|
|
51
|
+
modifier: Modifier = Modifier
|
|
52
|
+
) {
|
|
53
|
+
Card(
|
|
54
|
+
modifier = modifier.clickable(onClick = onClick).padding(8.dp),
|
|
55
|
+
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
|
56
|
+
) {
|
|
57
|
+
Column(
|
|
58
|
+
modifier = Modifier.padding(16.dp),
|
|
59
|
+
horizontalAlignment = Alignment.CenterHorizontally
|
|
60
|
+
) {
|
|
61
|
+
Icon(painter = icon, contentDescription = title)
|
|
62
|
+
Text(text = title, style = MaterialTheme.typography.bodyMedium)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 状态响应式组件
|
|
69
|
+
|
|
70
|
+
```kotlin
|
|
71
|
+
@Composable
|
|
72
|
+
fun StatusPanel(
|
|
73
|
+
status: LoadStatus,
|
|
74
|
+
modifier: Modifier = Modifier,
|
|
75
|
+
content: @Composable () -> Unit
|
|
76
|
+
) {
|
|
77
|
+
when (status) {
|
|
78
|
+
is LoadStatus.Loading -> LoadingIndicator(modifier = modifier)
|
|
79
|
+
is LoadStatus.Success -> content()
|
|
80
|
+
is LoadStatus.Error -> ErrorMessage(status.message, modifier = modifier)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 切换组件
|
|
86
|
+
|
|
87
|
+
```kotlin
|
|
88
|
+
@Composable
|
|
89
|
+
fun SettingSwitch(
|
|
90
|
+
title: String,
|
|
91
|
+
checked: Boolean,
|
|
92
|
+
onCheckedChange: (Boolean) -> Unit,
|
|
93
|
+
modifier: Modifier = Modifier
|
|
94
|
+
) {
|
|
95
|
+
Row(
|
|
96
|
+
modifier = modifier
|
|
97
|
+
.fillMaxWidth()
|
|
98
|
+
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
99
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
100
|
+
verticalAlignment = Alignment.CenterVertically
|
|
101
|
+
) {
|
|
102
|
+
Text(text = title, style = MaterialTheme.typography.bodyLarge)
|
|
103
|
+
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 设计系统文件组织
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
ui/
|
|
112
|
+
├── components/ ← 通用可复用组件
|
|
113
|
+
│ ├── FeatureCard.kt
|
|
114
|
+
│ ├── StatusPanel.kt
|
|
115
|
+
│ └── SettingSwitch.kt
|
|
116
|
+
├── theme/ ← 主题定义
|
|
117
|
+
│ ├── Color.kt
|
|
118
|
+
│ ├── Type.kt
|
|
119
|
+
│ └── Theme.kt
|
|
120
|
+
└── screens/ ← 页面级 Composable
|
|
121
|
+
├── HomeScreen.kt
|
|
122
|
+
└── SettingsScreen.kt
|
|
123
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Compose 性能优化配方
|
|
2
|
+
|
|
3
|
+
## 1. LazyColumn key 优化
|
|
4
|
+
|
|
5
|
+
```kotlin
|
|
6
|
+
// ❌ 无 key:列表项变更时全量重组
|
|
7
|
+
LazyColumn {
|
|
8
|
+
items(users) { user -> UserItem(user) }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ✅ 有 key:只有变更项重组
|
|
12
|
+
LazyColumn {
|
|
13
|
+
items(items = users, key = { it.id }) { user -> UserItem(user) }
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**影响**:带 key 时 Compose 可以精准识别哪项变了,避免不必要的重组和动画重置。
|
|
18
|
+
|
|
19
|
+
## 2. derivedStateOf 避免级联重组
|
|
20
|
+
|
|
21
|
+
```kotlin
|
|
22
|
+
// ❌ 每次 listState 变化都重组
|
|
23
|
+
val showButton = listState.firstVisibleItemIndex > 5
|
|
24
|
+
|
|
25
|
+
// ✅ 只在阈值跨越时触发
|
|
26
|
+
val showButton by remember {
|
|
27
|
+
derivedStateOf { listState.firstVisibleItemIndex > 5 }
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 3. Lambda 稳定性
|
|
32
|
+
|
|
33
|
+
```kotlin
|
|
34
|
+
// ❌ 每次重组创建新 lambda,子组件无法跳过
|
|
35
|
+
UserList(onClick = { user -> selectUser(user) })
|
|
36
|
+
|
|
37
|
+
// ✅ 方法引用,稳定不变
|
|
38
|
+
UserList(onClick = viewModel::selectUser)
|
|
39
|
+
|
|
40
|
+
// ✅ remember 包装
|
|
41
|
+
val onClick = remember(viewModel) { { user: User -> viewModel.selectUser(user) } }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 4. 不稳定参数稳定化
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ❌ MutableList 不稳定
|
|
48
|
+
@Composable
|
|
49
|
+
fun ItemList(items: MutableList<Item>) { ... }
|
|
50
|
+
|
|
51
|
+
// ✅ 使用不可变 List
|
|
52
|
+
@Composable
|
|
53
|
+
fun ItemList(items: List<Item>) { ... }
|
|
54
|
+
|
|
55
|
+
// ✅ 对自定义类添加 @Stable 注解
|
|
56
|
+
@Stable
|
|
57
|
+
data class UiState(val items: List<Item> = emptyList(), val isLoading: Boolean = false)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 5. 读取时机优化
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
// ❌ 过早读取:Lambda 外部读取状态,Lambda 每次都被重建
|
|
64
|
+
val color = MaterialTheme.colorScheme.primary
|
|
65
|
+
Box(modifier = Modifier.background(color).clickable { /* ... */ })
|
|
66
|
+
|
|
67
|
+
// ✅ 延迟读取:Lambda 内部读取,只在需要时触发
|
|
68
|
+
Box(modifier = Modifier.background(MaterialTheme.colorScheme.primary).clickable { /* ... */ })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 6. 避免向后写入
|
|
72
|
+
|
|
73
|
+
```kotlin
|
|
74
|
+
// ❌ 向后写入:组合期间修改状态
|
|
75
|
+
@Composable
|
|
76
|
+
fun BadExample() {
|
|
77
|
+
var text by remember { mutableStateOf("") }
|
|
78
|
+
text = "computed" // 组合期间写入,触发额外重组
|
|
79
|
+
Text(text)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ✅ 正确:使用派生状态
|
|
83
|
+
@Composable
|
|
84
|
+
fun GoodExample() {
|
|
85
|
+
val text by remember { derivedStateOf { "computed" } }
|
|
86
|
+
Text(text)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 7. remember 与 rememberSaveable 选择
|
|
91
|
+
|
|
92
|
+
| 场景 | 选择 | 原因 |
|
|
93
|
+
|------|------|------|
|
|
94
|
+
| 纯计算缓存 | `remember` | 无需跨配置变更保持 |
|
|
95
|
+
| 用户输入状态 | `rememberSaveable` | 屏幕旋转后恢复 |
|
|
96
|
+
| 滚动位置 | `rememberSaveable`(Saver) | 配置变更后恢复位置 |
|
|
97
|
+
| 临时动画状态 | `remember` | 动画重新启动可接受 |
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 状态管理模式集
|
|
2
|
+
|
|
3
|
+
## ViewModel + StateFlow(MVI 模式)
|
|
4
|
+
|
|
5
|
+
```kotlin
|
|
6
|
+
@HiltViewModel
|
|
7
|
+
class UserViewModel @Inject constructor(
|
|
8
|
+
private val userRepository: UserRepository
|
|
9
|
+
) : BaseViewModel<UserContract.State, UserContract.Event, UserContract.Effect, UserContract.Intent>() {
|
|
10
|
+
|
|
11
|
+
override val initialState = UserContract.State()
|
|
12
|
+
|
|
13
|
+
override val reducer = Reducer<UserContract.State, UserContract.Event> { state, event ->
|
|
14
|
+
when (event) {
|
|
15
|
+
is Event.NameChanged -> state.copy(name = event.name)
|
|
16
|
+
is Event.Loaded -> state.copy(isLoading = false, user = event.user)
|
|
17
|
+
is Event.Error -> state.copy(isLoading = false, error = event.message)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun handleIntent(intent: UserContract.Intent) {
|
|
22
|
+
when (intent) {
|
|
23
|
+
is Intent.UpdateName -> dispatch(Event.NameChanged(intent.name))
|
|
24
|
+
is Intent.Save -> { dispatch(Event.Loading); /* useCase call */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## remember / rememberSaveable
|
|
31
|
+
|
|
32
|
+
```kotlin
|
|
33
|
+
// remember:重组时保持状态,配置变更后丢失
|
|
34
|
+
var count by remember { mutableStateOf(0) }
|
|
35
|
+
|
|
36
|
+
// rememberSaveable:配置变更后仍保持
|
|
37
|
+
var query by rememberSaveable { mutableStateOf("") }
|
|
38
|
+
|
|
39
|
+
// remember 带 key:key 变化时重新初始化
|
|
40
|
+
var count by remember(userId) { mutableStateOf(0) }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## derivedStateOf
|
|
44
|
+
|
|
45
|
+
```kotlin
|
|
46
|
+
// 当状态频繁变化但 UI 只需响应阈值时
|
|
47
|
+
val showScrollToTop by remember {
|
|
48
|
+
derivedStateOf { listState.firstVisibleItemIndex > 5 }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 计算型状态,依赖变化时才重新计算
|
|
52
|
+
val filteredItems by remember(items, query) {
|
|
53
|
+
derivedStateOf { items.filter { it.name.contains(query, ignoreCase = true) } }
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## collectAsStateWithLifecycle
|
|
58
|
+
|
|
59
|
+
```kotlin
|
|
60
|
+
// 在 Compose 中收集 ViewModel 状态(生命周期感知)
|
|
61
|
+
@Composable
|
|
62
|
+
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
|
|
63
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
64
|
+
MyContent(state = state, onIntent = { viewModel.process(it) })
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## CompositionLocal
|
|
69
|
+
|
|
70
|
+
```kotlin
|
|
71
|
+
// 定义
|
|
72
|
+
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
|
|
73
|
+
error("No SnackbarHostState provided")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 提供
|
|
77
|
+
@Composable
|
|
78
|
+
fun App() {
|
|
79
|
+
val snackbarHostState = remember { SnackbarHostState() }
|
|
80
|
+
CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) {
|
|
81
|
+
Content()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 消费
|
|
86
|
+
@Composable
|
|
87
|
+
fun SomeDeepComponent() {
|
|
88
|
+
val snackbarHostState = LocalSnackbarHost.current
|
|
89
|
+
// 使用 snackbarHostState
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**注意**:CompositionLocal 适用于隐式传递的 UI 基础设施(主题、SnackbarHost),不适用于业务数据。
|
package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-kotlin-coroutines
|
|
3
|
+
description: 在使用 Kotlin 协程、Flow、StateFlow 实现异步操作,或在 Android 应用中管理并发时使用。覆盖结构化并发、调度器、Flow 操作符、防抖节流和异常处理。刚性约束由 rules 加载,本技能聚焦实现模式。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kotlin 协程 — 实现指引
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
Android 中使用 Kotlin 协程和 Flow 的异步编程模式。
|
|
11
|
+
|
|
12
|
+
> **刚性约束已由 rules 加载,本技能不再重复。** 详见:
|
|
13
|
+
> - `harness-coroutines-scope` — GlobalScope 禁用、viewModelScope、runBlocking 禁用、collectAsStateWithLifecycle、repeatOnLifecycle
|
|
14
|
+
> - `harness-hilt-injection` — 调度器注入约束
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 结构化并发
|
|
19
|
+
|
|
20
|
+
```kotlin
|
|
21
|
+
// 并行操作,全部完成或全部失败
|
|
22
|
+
suspend fun loadDashboard(): Dashboard = coroutineScope {
|
|
23
|
+
val userDeferred = async { userRepository.getUser() }
|
|
24
|
+
val ordersDeferred = async { orderRepository.getOrders() }
|
|
25
|
+
Dashboard(user = userDeferred.await(), orders = ordersDeferred.await())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 带超时
|
|
29
|
+
suspend fun loadWithTimeout(): Data = withTimeout(5000) { api.fetchData() }
|
|
30
|
+
|
|
31
|
+
// 或超时返回 null
|
|
32
|
+
suspend fun loadOrNull(): Data? = withTimeoutOrNull(5000) { api.fetchData() }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 调度器
|
|
36
|
+
|
|
37
|
+
```kotlin
|
|
38
|
+
// IO — 网络、数据库、文件
|
|
39
|
+
withContext(Dispatchers.IO) { database.query() }
|
|
40
|
+
|
|
41
|
+
// Default — CPU 密集型
|
|
42
|
+
withContext(Dispatchers.Default) { expensiveComputation(data) }
|
|
43
|
+
|
|
44
|
+
// 注入调度器(推荐)
|
|
45
|
+
class MyRepository @Inject constructor(
|
|
46
|
+
@DispatcherIO private val ioDispatcher: CoroutineDispatcher
|
|
47
|
+
) {
|
|
48
|
+
fun getData(): Flow<Data> = flow { emit(api.fetchData()) }.flowOn(ioDispatcher)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Flow 操作符
|
|
53
|
+
|
|
54
|
+
```kotlin
|
|
55
|
+
// 转换 + 去重
|
|
56
|
+
userRepository.getUsers()
|
|
57
|
+
.map { users -> users.filter { it.isActive } }
|
|
58
|
+
.distinctUntilChanged()
|
|
59
|
+
.collect { activeUsers -> updateUI(activeUsers) }
|
|
60
|
+
|
|
61
|
+
// 组合多个 Flow
|
|
62
|
+
val combined = combine(userFlow, settingsFlow) { user, settings -> Pair(user, settings) }
|
|
63
|
+
|
|
64
|
+
// 搜索模式
|
|
65
|
+
searchQuery
|
|
66
|
+
.debounce(300)
|
|
67
|
+
.flatMapLatest { query -> if (query.isEmpty()) flowOf(emptyList()) else searchRepository.search(query) }
|
|
68
|
+
.collect { results -> updateResults(results) }
|
|
69
|
+
|
|
70
|
+
// 带指数退避的重试
|
|
71
|
+
api.fetchData().retry(3) { cause ->
|
|
72
|
+
if (cause is IOException) { delay(1000L * (2.0.pow(retryCount)).toLong()); true } else false
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 异常处理
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
// runCatching 模式
|
|
80
|
+
suspend fun safeApiCall(): Result<User> = runCatching { api.getUser() }
|
|
81
|
+
|
|
82
|
+
// ViewModel 中处理
|
|
83
|
+
fun loadUser() {
|
|
84
|
+
viewModelScope.launch {
|
|
85
|
+
safeApiCall()
|
|
86
|
+
.onSuccess { user -> _uiState.update { it.copy(data = user) } }
|
|
87
|
+
.onFailure { error -> _uiState.update { it.copy(error = error.message) } }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// SupervisorJob:子任务独立失败
|
|
92
|
+
class MyViewModel : ViewModel() {
|
|
93
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
94
|
+
fun loadMultiple() {
|
|
95
|
+
scope.launch { userRepository.getUser() } // 失败不影响另一个
|
|
96
|
+
scope.launch { orderRepository.getOrders() }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 防抖与节流
|
|
102
|
+
|
|
103
|
+
```kotlin
|
|
104
|
+
// Compose 中的防抖搜索
|
|
105
|
+
@Composable
|
|
106
|
+
fun SearchField(onSearch: (String) -> Unit) {
|
|
107
|
+
var query by remember { mutableStateOf("") }
|
|
108
|
+
LaunchedEffect(query) {
|
|
109
|
+
delay(300) // 防抖
|
|
110
|
+
if (query.isNotEmpty()) onSearch(query)
|
|
111
|
+
}
|
|
112
|
+
TextField(value = query, onValueChange = { query = it })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ViewModel 中的 StateFlow 搜索
|
|
116
|
+
val searchResults = _searchQuery
|
|
117
|
+
.debounce(300)
|
|
118
|
+
.distinctUntilChanged()
|
|
119
|
+
.flatMapLatest { query -> searchRepository.search(query) }
|
|
120
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 取消处理
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
suspend fun downloadFile(url: String): ByteArray = withContext(Dispatchers.IO) {
|
|
127
|
+
val connection = URL(url).openConnection()
|
|
128
|
+
connection.inputStream.use { input ->
|
|
129
|
+
val buffer = ByteArrayOutputStream()
|
|
130
|
+
val data = ByteArray(4096)
|
|
131
|
+
while (true) {
|
|
132
|
+
ensureActive() // 检查取消
|
|
133
|
+
val count = input.read(data)
|
|
134
|
+
if (count == -1) break
|
|
135
|
+
buffer.write(data, 0, count)
|
|
136
|
+
}
|
|
137
|
+
buffer.toByteArray()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 可取消的轮询 Flow
|
|
142
|
+
fun pollData(): Flow<Data> = flow {
|
|
143
|
+
while (currentCoroutineContext().isActive) {
|
|
144
|
+
emit(api.fetchData())
|
|
145
|
+
delay(5000)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 反模式速查
|
|
151
|
+
|
|
152
|
+
| 反模式 | 正确做法 |
|
|
153
|
+
|--------|---------|
|
|
154
|
+
| `GlobalScope.launch { ... }` | `viewModelScope.launch { ... }` |
|
|
155
|
+
| `runBlocking { api.fetch() }` | `viewModelScope.launch { withContext(IO) { api.fetch() } }` |
|
|
156
|
+
| `lifecycleScope.launch { flow.collect {} }` | `repeatOnLifecycle(STARTED) { flow.collect {} }` |
|
|
157
|
+
| 每次调用创建新 Flow | `stateIn(viewModelScope, WhileSubscribed(5000), initial)` |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 必须遵守的约束规则
|
|
162
|
+
|
|
163
|
+
> rules 引用路径:`../rules/<rule-name>.mdc`
|
|
164
|
+
|
|
165
|
+
- harness-coroutines-scope
|
|
166
|
+
- harness-hilt-injection
|
|
167
|
+
- harness-mvi-layering
|