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.
Files changed (134) hide show
  1. package/README.md +531 -0
  2. package/bin/cli.js +3 -0
  3. package/dist/adapters/_frontmatter.js +24 -0
  4. package/dist/adapters/claude-code.js +12 -0
  5. package/dist/adapters/codechat.js +12 -0
  6. package/dist/adapters/cursor.js +19 -0
  7. package/dist/adapters/generic.js +19 -0
  8. package/dist/adapters/index.js +26 -0
  9. package/dist/adapters/qoder.js +12 -0
  10. package/dist/commands/apply.js +272 -0
  11. package/dist/commands/init.js +420 -0
  12. package/dist/core/agent-injector.js +192 -0
  13. package/dist/core/next-steps.js +91 -0
  14. package/dist/core/render-meta.js +81 -0
  15. package/dist/core/repomix-pack.js +54 -0
  16. package/dist/core/scaffold.js +93 -0
  17. package/dist/core/state.js +80 -0
  18. package/dist/index.js +239 -0
  19. package/dist/types.js +5 -0
  20. package/dist/utils/baseline-copy.js +591 -0
  21. package/dist/utils/baseline-defaults.js +106 -0
  22. package/dist/utils/logger.js +56 -0
  23. package/dist/utils/validate-args.js +132 -0
  24. package/dist/utils/version.js +23 -0
  25. package/dist/wiki/abort.js +30 -0
  26. package/dist/wiki/config.js +79 -0
  27. package/dist/wiki/defaults.js +16 -0
  28. package/dist/wiki/envLoader.js +78 -0
  29. package/dist/wiki/index.js +29 -0
  30. package/dist/wiki/openaiCompat.js +219 -0
  31. package/dist/wiki/repowikiCanonicalSections.js +67 -0
  32. package/dist/wiki/repowikiCheckpoint.js +106 -0
  33. package/dist/wiki/repowikiConfig.js +9 -0
  34. package/dist/wiki/repowikiGit.js +73 -0
  35. package/dist/wiki/repowikiIndexer.js +824 -0
  36. package/dist/wiki/repowikiMarkdownPost.js +123 -0
  37. package/dist/wiki/repowikiMetadataContent.js +64 -0
  38. package/dist/wiki/repowikiMetadataJson.js +15 -0
  39. package/dist/wiki/repowikiScanner.js +156 -0
  40. package/dist/wiki/repowikiStructureNav.js +286 -0
  41. package/dist/wiki/repowikiStructureNormalize.js +218 -0
  42. package/dist/wiki/wikiStructureXml.js +316 -0
  43. package/dist/wiki/wikiTasksWriter.js +127 -0
  44. package/package.json +57 -0
  45. package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
  46. package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
  47. package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
  48. package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
  49. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
  50. package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
  51. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
  52. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
  53. package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
  54. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
  55. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
  56. package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
  57. package/templates/_shared/meta/AGENTS.md.ejs +53 -0
  58. package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
  59. package/templates/_shared/meta/README.md.ejs +51 -0
  60. package/templates/_shared/meta/VERSION.ejs +1 -0
  61. package/templates/_shared/meta/harness.yaml.ejs +52 -0
  62. package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
  63. package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
  64. package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
  65. package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
  66. package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
  67. package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
  68. package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
  69. package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
  70. package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
  71. package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
  72. package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
  73. package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
  74. package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
  75. package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
  76. package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
  77. package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
  78. package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
  79. package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
  80. package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
  81. package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
  82. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
  83. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
  84. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
  85. package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
  86. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
  87. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
  88. package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
  89. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
  90. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
  91. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
  92. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
  93. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
  94. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
  95. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
  96. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
  97. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
  98. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
  99. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
  100. package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
  101. package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
  102. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
  103. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
  104. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
  105. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
  106. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
  107. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
  108. package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
  109. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
  110. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
  111. 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
  112. 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
  113. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
  114. package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
  115. package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
  116. package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
  117. package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
  118. package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
  119. package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
  120. package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
  121. package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
  122. package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
  123. package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
  124. package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
  125. package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
  126. package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
  127. package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
  128. package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
  129. package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
  130. package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
  131. package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
  132. package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
  133. package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
  134. 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),不适用于业务数据。
@@ -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