@michaelbel/cuckcoder-mcp 1.6.11
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/assets/rules/architecture.md +15 -0
- package/assets/rules/bottom-sheet.md +20 -0
- package/assets/rules/compose-color.md +22 -0
- package/assets/rules/compose-constraintlayout.md +18 -0
- package/assets/rules/compose-screen.md +33 -0
- package/assets/rules/compose-spacing.md +39 -0
- package/assets/rules/compose.md +54 -0
- package/assets/rules/dialog.md +33 -0
- package/assets/rules/domain.md +36 -0
- package/assets/rules/filesystem.md +3 -0
- package/assets/rules/git.md +11 -0
- package/assets/rules/github-readme.md +97 -0
- package/assets/rules/github-repo.md +65 -0
- package/assets/rules/kmp.md +14 -0
- package/assets/rules/kotlin.md +48 -0
- package/assets/rules/lazylist.md +36 -0
- package/assets/rules/mvi-error-handling.md +19 -0
- package/assets/rules/mvi-state.md +31 -0
- package/assets/rules/mvi.md +39 -0
- package/assets/rules/navigation.md +20 -0
- package/assets/rules/network.md +36 -0
- package/assets/rules/preview.md +31 -0
- package/assets/rules/realtime.md +39 -0
- package/assets/rules/resource.md +18 -0
- package/assets/rules/room.md +33 -0
- package/assets/rules/scaffold.md +25 -0
- package/assets/rules/shimmer.md +34 -0
- package/assets/rules/textfield.md +31 -0
- package/assets/rules/typography.md +32 -0
- package/assets/rules/usecase.md +60 -0
- package/assets/rules/workflow.md +17 -0
- package/assets/rules/workmanager.md +44 -0
- package/assets/skills/create-data-layer/SKILL.md +60 -0
- package/assets/skills/create-datastore-preference/SKILL.md +80 -0
- package/assets/skills/create-domain-mapper/SKILL.md +30 -0
- package/assets/skills/create-feature-alert-dialog/SKILL.md +549 -0
- package/assets/skills/create-feature-bottom-sheet/SKILL.md +300 -0
- package/assets/skills/create-feature-scaffold-screen/SKILL.md +74 -0
- package/assets/skills/create-feature-scaffold-screen/references/form-screen.md +20 -0
- package/assets/skills/create-feature-scaffold-screen/references/network-only-screen.md +20 -0
- package/assets/skills/create-feature-scaffold-screen/references/paginated-screen.md +29 -0
- package/assets/skills/create-feature-scaffold-screen/references/room-backed-screen.md +23 -0
- package/assets/skills/create-feature-scaffold-screen/references/static-screen.md +13 -0
- package/assets/skills/create-guide/SKILL.md +169 -0
- package/assets/skills/create-guide/references/android-project.md +120 -0
- package/assets/skills/create-guide/references/androidx.md +49 -0
- package/assets/skills/create-guide/references/existing-guides.yaml +60 -0
- package/assets/skills/create-guide/references/github.md +71 -0
- package/assets/skills/create-guide/references/guide-defaults.yaml +51 -0
- package/assets/skills/create-guide/references/notion.md +121 -0
- package/assets/skills/create-guide/references/output-contract.md +79 -0
- package/assets/skills/create-guide/references/research.md +64 -0
- package/assets/skills/create-guide/references/validation.md +59 -0
- package/assets/skills/create-guide/scripts/validate_guide.py +167 -0
- package/assets/skills/create-guide/templates/guide-manifest.yaml +67 -0
- package/assets/skills/create-guide/templates/implementation-plan.md +30 -0
- package/assets/skills/create-guide/templates/notion-page.md +46 -0
- package/assets/skills/create-guide/templates/repository/AGENTS.md +7 -0
- package/assets/skills/create-guide/templates/repository/README.md +32 -0
- package/assets/skills/create-guide/templates/repository/SOURCES.md +32 -0
- package/assets/skills/create-guide/templates/validation-report.md +30 -0
- package/assets/skills/create-guide/tests/test_validate_guide.py +97 -0
- package/assets/skills/create-ktor-endpoint/SKILL.md +33 -0
- package/assets/skills/create-notification-flow/SKILL.md +103 -0
- package/assets/skills/create-offline-outbox/SKILL.md +110 -0
- package/assets/skills/create-paging-flow/SKILL.md +96 -0
- package/assets/skills/create-project-from-template/SKILL.md +91 -0
- package/assets/skills/create-project-from-template/assets/icons/android.svg +12 -0
- package/assets/skills/create-project-from-template/assets/icons/compose.svg +34 -0
- package/assets/skills/create-project-from-template/assets/icons/jetpack.svg +10 -0
- package/assets/skills/create-project-from-template/references/myapplication-checklist.md +92 -0
- package/assets/skills/create-room-storage/SKILL.md +39 -0
- package/assets/skills/create-shared-component/SKILL.md +160 -0
- package/assets/skills/create-signalr-channel/SKILL.md +39 -0
- package/assets/skills/create-usecase/SKILL.md +142 -0
- package/assets/skills/create-workmanager-task/SKILL.md +41 -0
- package/assets/skills/github-repo-settings/SKILL.md +152 -0
- package/assets/skills/interview-me/SKILL.md +150 -0
- package/dist/errors.js +49 -0
- package/dist/frontmatter.js +44 -0
- package/dist/github.js +23 -0
- package/dist/index.js +6 -0
- package/dist/server.js +131 -0
- package/dist/source/bundled.js +78 -0
- package/dist/source/cache.js +61 -0
- package/dist/source/github-source.js +68 -0
- package/dist/source/github.js +185 -0
- package/dist/source/index.js +36 -0
- package/dist/source/types.js +1 -0
- package/dist/validation.js +33 -0
- package/dist/version.js +26 -0
- package/package.json +38 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-feature-bottom-sheet
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит создать Compose bottom sheet, modal sheet фичу или пакет `_sheet`,
|
|
5
|
+
или говорит "create a bottom sheet", "add a modal sheet", "new ModalBottomSheet". Покрывает
|
|
6
|
+
раскладку пакета `{feature}_sheet`, `SharedModalBottomSheet`, `rememberModalBottomSheetState` и
|
|
7
|
+
его preview. Не используй для диалога с простыми кнопками confirm/dismiss; используй вместо этого
|
|
8
|
+
[create-feature-alert-dialog](../create-feature-alert-dialog/SKILL.md). Не используй для полного
|
|
9
|
+
навигируемого экрана; используй вместо этого
|
|
10
|
+
[create-feature-scaffold-screen](../create-feature-scaffold-screen/SKILL.md).
|
|
11
|
+
metadata:
|
|
12
|
+
author: michaelbel
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Новый Bottom Sheet
|
|
16
|
+
|
|
17
|
+
Требует зависимость `androidx.compose.material3:material3`.
|
|
18
|
+
|
|
19
|
+
Создаёт Compose bottom sheet проекта. Замени `{Feature}` на назначение sheet, `{feature}` на имя в
|
|
20
|
+
lower camel case, а `{package}` на целевой пакет.
|
|
21
|
+
|
|
22
|
+
## Фаза 1: создать папку фичи
|
|
23
|
+
|
|
24
|
+
Создай папку `features/{feature}_sheet` — постфикс `_sheet` обязателен, даже если фича сама по
|
|
25
|
+
себе не заканчивается на «шит».
|
|
26
|
+
|
|
27
|
+
## Фаза 2: Intent и Model
|
|
28
|
+
|
|
29
|
+
Внутри `features/{feature}_sheet` создай папку `intent`. Папку `model` создавай только когда sheet
|
|
30
|
+
нужны отображаемые данные; если данных нет, не создавай ни папку, ни файл Model.
|
|
31
|
+
|
|
32
|
+
Файлы:
|
|
33
|
+
- `features/{feature}_sheet/{Feature}BottomSheet.kt`
|
|
34
|
+
- `features/{feature}_sheet/intent/{Feature}SheetIntent.kt`
|
|
35
|
+
- optional `features/{feature}_sheet/model/{Feature}SheetModel.kt`
|
|
36
|
+
|
|
37
|
+
### {Feature}SheetIntent.kt
|
|
38
|
+
|
|
39
|
+
```kotlin
|
|
40
|
+
package {package}.features.{feature}_sheet.intent
|
|
41
|
+
|
|
42
|
+
import {package}.shared.mvi.Intent
|
|
43
|
+
|
|
44
|
+
sealed interface {Feature}SheetIntent: Intent {
|
|
45
|
+
data object DismissClick: {Feature}SheetIntent
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Правила:
|
|
50
|
+
- `DismissClick` присутствует всегда и всегда идёт первым интентом в `sealed interface`.
|
|
51
|
+
- Для остальных действий (клик по элементу, primary/secondary action и т. д.) добавляй свои
|
|
52
|
+
`data object` / `data class` записи; `data object` всегда идёт перед `data class`.
|
|
53
|
+
|
|
54
|
+
### {Feature}SheetModel.kt (только если есть данные)
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
package {package}.features.{feature}_sheet.model
|
|
58
|
+
|
|
59
|
+
import {package}.shared.mvi.Model
|
|
60
|
+
|
|
61
|
+
data class {Feature}SheetModel(
|
|
62
|
+
val showPrimaryAction: Boolean = false
|
|
63
|
+
): Model
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## {Feature}BottomSheet.kt
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
package {package}
|
|
70
|
+
|
|
71
|
+
// Добавь все необходимые импорты
|
|
72
|
+
|
|
73
|
+
@Composable
|
|
74
|
+
fun {Feature}BottomSheet(
|
|
75
|
+
state: {Feature}SheetModel,
|
|
76
|
+
dispatch: ({Feature}SheetIntent) -> Unit
|
|
77
|
+
) {
|
|
78
|
+
SharedModalBottomSheet(
|
|
79
|
+
onDismissRequest = { dispatch({Feature}SheetIntent.DismissClick) }
|
|
80
|
+
) {
|
|
81
|
+
SharedLazyColumn(
|
|
82
|
+
modifier = Modifier.fillMaxWidth(),
|
|
83
|
+
contentPadding = PaddingValues(
|
|
84
|
+
start = 16.dp,
|
|
85
|
+
top = 44.dp,
|
|
86
|
+
end = 16.dp,
|
|
87
|
+
bottom = 16.dp
|
|
88
|
+
),
|
|
89
|
+
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
90
|
+
horizontalAlignment = Alignment.CenterHorizontally
|
|
91
|
+
) {
|
|
92
|
+
item {
|
|
93
|
+
{Feature}Card(
|
|
94
|
+
state = {Feature}CardState(
|
|
95
|
+
model = state.item,
|
|
96
|
+
onClick = { dispatch({Feature}SheetIntent.ItemClick) }
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
if (state.showSecondaryAction) {
|
|
101
|
+
item {
|
|
102
|
+
{Feature}SecondaryButton(
|
|
103
|
+
onClick = { dispatch({Feature}SheetIntent.SecondaryActionClick) }
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (state.showPrimaryAction) {
|
|
108
|
+
item {
|
|
109
|
+
Button(
|
|
110
|
+
onClick = { dispatch({Feature}SheetIntent.PrimaryActionClick) },
|
|
111
|
+
modifier = Modifier
|
|
112
|
+
.fillMaxWidth()
|
|
113
|
+
.height(56.dp),
|
|
114
|
+
shape = RoundedCornerShape(8.dp)
|
|
115
|
+
) {
|
|
116
|
+
SharedFixedText(
|
|
117
|
+
text = stringResource(AppStrings.{Feature}PrimaryAction),
|
|
118
|
+
style = MaterialTheme.typography.medium16.copy(
|
|
119
|
+
textAlign = TextAlign.Center
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@PreviewWrapper(ThemeWrapper::class)
|
|
130
|
+
@Preview
|
|
131
|
+
@Composable
|
|
132
|
+
private fun {Feature}BottomSheetPreview(
|
|
133
|
+
@PreviewParameter({Feature}SheetModelPreviewParameterProvider::class) state: {Feature}SheetModel
|
|
134
|
+
) {
|
|
135
|
+
Box(
|
|
136
|
+
modifier = Modifier.fillMaxSize()
|
|
137
|
+
) {
|
|
138
|
+
{Feature}BottomSheet(
|
|
139
|
+
state = state,
|
|
140
|
+
dispatch = {}
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private class {Feature}SheetModelPreviewParameterProvider: PreviewParameterProvider<{Feature}SheetModel> {
|
|
146
|
+
override val values: Sequence<{Feature}SheetModel>
|
|
147
|
+
get() {
|
|
148
|
+
val item = {Feature}ItemModel(
|
|
149
|
+
id = "sample-id",
|
|
150
|
+
title = "Sample item",
|
|
151
|
+
subtitle = "Sample details"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return sequenceOf(
|
|
155
|
+
{Feature}SheetModel(
|
|
156
|
+
item = item,
|
|
157
|
+
showPrimaryAction = true,
|
|
158
|
+
showSecondaryAction = false
|
|
159
|
+
),
|
|
160
|
+
{Feature}SheetModel(
|
|
161
|
+
item = item,
|
|
162
|
+
showPrimaryAction = false,
|
|
163
|
+
showSecondaryAction = true
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Правила:
|
|
171
|
+
- Используй обёртку `SharedModalBottomSheet`, а не сырой `ModalBottomSheet`.
|
|
172
|
+
- Не задавай `sheetState` вручную в фича-коде — `SharedModalBottomSheet` уже использует
|
|
173
|
+
`rememberModalBottomSheetState(skipPartiallyExpanded = true)` по умолчанию.
|
|
174
|
+
- Не задавай `containerColor`, `sheetGesturesEnabled` или `dragHandle` — у `SharedModalBottomSheet`
|
|
175
|
+
уже есть нужные умолчания (`dragHandle` всегда рисует `SharedDragHandle()`).
|
|
176
|
+
- Делай preview composable, который сам рендерит bottom sheet, а не отдельный приватный composable
|
|
177
|
+
только с содержимым.
|
|
178
|
+
- Оборачивай preview bottom sheet в `Box(modifier = Modifier.fillMaxSize())`; иначе preview может
|
|
179
|
+
не отрендериться.
|
|
180
|
+
- Используй анонимизированные тестовые данные для preview, такие как `sample-id`, `Sample item` и
|
|
181
|
+
`Sample details`.
|
|
182
|
+
- Не добавляй пустые строки между соседними блоками `item {}` внутри `SharedLazyColumn`.
|
|
183
|
+
- Не добавляй `Spacer` без визуального назначения; используй `Spacer` только в конце списка, чтобы
|
|
184
|
+
создать отступ под последним элементом.
|
|
185
|
+
|
|
186
|
+
## Фаза 3: вызов sheet с экрана
|
|
187
|
+
|
|
188
|
+
Sheet не хранит собственный ViewModel — видимостью и данными управляет экран, который его
|
|
189
|
+
открывает.
|
|
190
|
+
|
|
191
|
+
### 1. Флаг видимости в Model экрана
|
|
192
|
+
|
|
193
|
+
В `Model` вызывающего экрана добавь `Boolean`-поле `is{Feature}SheetVisible` — `is`, имя sheet
|
|
194
|
+
(с постфиксом `Sheet`), затем `Visible`:
|
|
195
|
+
|
|
196
|
+
```kotlin
|
|
197
|
+
data class {Screen}Model(
|
|
198
|
+
val is{Feature}SheetVisible: Boolean = false
|
|
199
|
+
): Model
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 2. Показ sheet
|
|
203
|
+
|
|
204
|
+
Там, где решаешь открыть sheet, диспатчи `reduce`, выставляющий флаг в `true`:
|
|
205
|
+
|
|
206
|
+
```kotlin
|
|
207
|
+
reduce { it.copy(is{Feature}SheetVisible = true) }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 3. Обработка intent'ов sheet в ViewModel экрана
|
|
211
|
+
|
|
212
|
+
Добавь в `{Screen}Intent` экрана один case `On{Feature}SheetIntent`, оборачивающий весь
|
|
213
|
+
`{Feature}SheetIntent` целиком — отдельный case на каждый intent sheet не создавай:
|
|
214
|
+
|
|
215
|
+
```kotlin
|
|
216
|
+
sealed interface {Screen}Intent: Intent {
|
|
217
|
+
data class On{Feature}SheetIntent(val intent: {Feature}SheetIntent): {Screen}Intent
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Разбирай `intent.intent` вложенным `when` внутри `dispatch` ViewModel экрана:
|
|
222
|
+
|
|
223
|
+
```kotlin
|
|
224
|
+
override fun dispatch(intent: {Screen}Intent) {
|
|
225
|
+
when (intent) {
|
|
226
|
+
is {Screen}Intent.On{Feature}SheetIntent -> {
|
|
227
|
+
when (intent.intent) {
|
|
228
|
+
is {Feature}SheetIntent.DismissClick -> {
|
|
229
|
+
reduce { it.copy(is{Feature}SheetVisible = false) }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 4. Отрисовка
|
|
238
|
+
|
|
239
|
+
Рендери sheet внутри первой, публичной функции экрана (`{Screen}Screen`, не
|
|
240
|
+
`{Screen}ScreenContent`), сразу после вызова `{Screen}ScreenContent(...)`. В `dispatch`-лямбде sheet
|
|
241
|
+
просто пробрасывай весь intent в `On{Feature}SheetIntent`, не разбирая его case'ы в самом
|
|
242
|
+
composable:
|
|
243
|
+
|
|
244
|
+
```kotlin
|
|
245
|
+
@Composable
|
|
246
|
+
fun {Screen}Screen(
|
|
247
|
+
viewModel: {Screen}ViewModel = hiltViewModel()
|
|
248
|
+
) {
|
|
249
|
+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
|
250
|
+
|
|
251
|
+
{Screen}ScreenContent(
|
|
252
|
+
state = state,
|
|
253
|
+
dispatch = viewModel::dispatch
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if (state.is{Feature}SheetVisible) {
|
|
257
|
+
{Feature}BottomSheet(
|
|
258
|
+
dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}SheetIntent(intent)) }
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Если у sheet есть Model
|
|
265
|
+
|
|
266
|
+
Когда sheet принимает `state`, не храни отдельную копию его Model в экране — вычисляй её свойством
|
|
267
|
+
`get()` из уже существующих данных экрана. Имя свойства оканчивается на `State`, а не на `Model`
|
|
268
|
+
(сам тип остаётся `{Feature}SheetModel`):
|
|
269
|
+
|
|
270
|
+
```kotlin
|
|
271
|
+
data class {Screen}Model(
|
|
272
|
+
val {feature}Item: {Feature}Item = {Feature}Item.Empty,
|
|
273
|
+
val is{Feature}SheetVisible: Boolean = false
|
|
274
|
+
): Model {
|
|
275
|
+
val {feature}SheetState: {Feature}SheetModel
|
|
276
|
+
get() = {Feature}SheetModel(
|
|
277
|
+
item = {feature}Item
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```kotlin
|
|
283
|
+
if (state.is{Feature}SheetVisible) {
|
|
284
|
+
{Feature}BottomSheet(
|
|
285
|
+
state = state.{feature}SheetState,
|
|
286
|
+
dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}SheetIntent(intent)) }
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Правила:
|
|
292
|
+
- Sheet не хранит собственный ViewModel; видимостью управляет `Boolean`-поле
|
|
293
|
+
`is{Feature}SheetVisible` в `Model` вызывающего экрана.
|
|
294
|
+
- Экран оборачивает весь `{Feature}SheetIntent` в один case `On{Feature}SheetIntent(val intent:
|
|
295
|
+
{Feature}SheetIntent)` своего `{Screen}Intent`; отдельных case'ов на каждый intent sheet не
|
|
296
|
+
создавай — разбирай их вложенным `when` внутри `dispatch` ViewModel.
|
|
297
|
+
- Рендери sheet в первой, публичной функции экрана, сразу после вызова
|
|
298
|
+
`{Screen}ScreenContent(...)`.
|
|
299
|
+
- Если sheet нужна Model, вычисляй её свойством `get()` в `Model` экрана из уже существующих
|
|
300
|
+
данных — не храни отдельную копию; имя свойства оканчивается на `State`, а не на `Model`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-feature-scaffold-screen
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит создать новый Android Compose-экран или MVI-фичу с
|
|
5
|
+
Screen/ViewModel/Intent/Model/Event и опциональным route. Поддерживает static, network-only,
|
|
6
|
+
Room-backed collect/load, form и paginated экраны. Не используй для отдельного переиспользуемого
|
|
7
|
+
компонента, alert dialog или bottom sheet; используй create-shared-component,
|
|
8
|
+
create-feature-alert-dialog или create-feature-bottom-sheet. Не используй только для data/domain
|
|
9
|
+
слоя без экрана.
|
|
10
|
+
metadata:
|
|
11
|
+
author: michaelbel
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Новый MVI-экран
|
|
15
|
+
|
|
16
|
+
Создай экран в `features/{feature}`, где `{feature}` — snake_case, `{Feature}` — PascalCase, а
|
|
17
|
+
`{package}` — пакет целевого проекта.
|
|
18
|
+
|
|
19
|
+
Обычный набор файлов:
|
|
20
|
+
|
|
21
|
+
- `{Feature}Screen.kt`
|
|
22
|
+
- `{Feature}ViewModel.kt`
|
|
23
|
+
- `model/{Feature}Model.kt`
|
|
24
|
+
- `intent/{Feature}Intent.kt`
|
|
25
|
+
- `event/{Feature}Event.kt`, только если нужны одноразовые эффекты
|
|
26
|
+
- `navigation/{Feature}Route.kt`, только если экран участвует в навигации
|
|
27
|
+
|
|
28
|
+
## Выбери режим
|
|
29
|
+
|
|
30
|
+
Изучи ближайший аналогичный экран и выбери основной режим. Затем обязательно прочитай только его
|
|
31
|
+
reference. Для гибридного экрана прочитай references только для реально объединяемых режимов.
|
|
32
|
+
|
|
33
|
+
| Режим | Когда выбирать | Reference |
|
|
34
|
+
| ------------------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
35
|
+
| static | Нет загрузки данных; экран отображает заданное состояние и локальные действия | [static-screen.md](references/static-screen.md) |
|
|
36
|
+
| network-only | Одноразовый сетевой результат сразу становится состоянием экрана, без Room | [network-only-screen.md](references/network-only-screen.md) |
|
|
37
|
+
| Room-backed collect/load | Room — источник истины, сеть только обновляет его | [room-backed-screen.md](references/room-backed-screen.md) |
|
|
38
|
+
| form | Пользователь редактирует поля и отправляет значения | [form-screen.md](references/form-screen.md) |
|
|
39
|
+
| paginated | Экран отображает `PagingData` и load states | [paginated-screen.md](references/paginated-screen.md) |
|
|
40
|
+
|
|
41
|
+
Если нужный endpoint, storage, mapper или use case отсутствует, сначала примени соответствующий
|
|
42
|
+
атомарный skill либо `create-data-layer` для составного потока. Экранный skill не создаёт эти слои
|
|
43
|
+
скрыто.
|
|
44
|
+
|
|
45
|
+
## Общие MVI-инварианты
|
|
46
|
+
|
|
47
|
+
- Всё изменяемое UI-состояние хранится в `{Feature}Model` и меняется только через
|
|
48
|
+
`reduce { it.copy(...) }`. Не держи дублирующие mutable-поля во ViewModel или Composable.
|
|
49
|
+
- Исключение для Paging — публичный immutable `Flow<PagingData<T>>`; параметры, влияющие на этот
|
|
50
|
+
Flow, всё равно хранятся в Model.
|
|
51
|
+
- Каждый intent представляет одно действие пользователя или lifecycle-событие. В sealed interface
|
|
52
|
+
размещай `data object` перед `data class`.
|
|
53
|
+
- `dispatch` использует исчерпывающий `when` без `else`. Обрабатывай поведение прямо в его ветках;
|
|
54
|
+
не выноси ветки в приватные handler-функции.
|
|
55
|
+
- Внедряй конкретные `UseCase` / `FlowUseCase`, а не Repository, Interactor или агрегирующий фасад.
|
|
56
|
+
Одноразовые use case вызывай через `.getOrThrow()` внутри `launch { ... }`; исключения обрабатывай
|
|
57
|
+
в принятом проектом `catch` ViewModel.
|
|
58
|
+
- События применяй только для одноразовых эффектов: навигации, snackbar, диалога или команды
|
|
59
|
+
обновить Paging. Постоянные данные в Event не храни.
|
|
60
|
+
- Публичный `{Feature}Screen(viewModel = hiltViewModel())` собирает state через
|
|
61
|
+
`collectAsStateWithLifecycle()`, наблюдает event flow при его наличии и делегирует UI приватному
|
|
62
|
+
`{Feature}ScreenContent`.
|
|
63
|
+
- `{Feature}ScreenContent` получает state/data и callback `dispatch`; он не знает о ViewModel.
|
|
64
|
+
Preview создавай для content через `@PreviewWrapper(ThemeWrapper::class)` и приватный
|
|
65
|
+
`PreviewParameterProvider`.
|
|
66
|
+
- Применяй `innerPadding` из `Scaffold` через `contentPadding` списка или `Modifier.padding` обычного
|
|
67
|
+
содержимого. Добавляй file-level opt-in только для реально используемых experimental API.
|
|
68
|
+
|
|
69
|
+
## Навигация и завершение
|
|
70
|
+
|
|
71
|
+
При необходимости создай route и зарегистрируй его в существующем navigation graph тем же
|
|
72
|
+
способом, что соседние фичи. Не вводи новую навигационную абстракцию. Проверь loading, content,
|
|
73
|
+
empty и error-состояния, которые действительно принадлежат выбранному режиму, preview-варианты и
|
|
74
|
+
сборку затронутого модуля.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Form screen
|
|
2
|
+
|
|
3
|
+
Используй этот режим для редактируемых полей, локальной валидации и отправки формы.
|
|
4
|
+
|
|
5
|
+
## Контракт
|
|
6
|
+
|
|
7
|
+
- Храни значения полей, ошибки и nullable submit-`Job` в Model; вычисляй `isSubmitting` из
|
|
8
|
+
активности `Job`. Не делай `remember` источником истины для бизнес-полей.
|
|
9
|
+
- Создай отдельный `data class` intent для изменения каждого логически независимого поля и
|
|
10
|
+
`data object` для Submit/Retry/Back. Сохрани порядок `data object` перед `data class`.
|
|
11
|
+
- В field-change ветке обновляй Model через `reduce`; очищай только ошибку изменённого поля, если
|
|
12
|
+
это соответствует UX.
|
|
13
|
+
- Перед submit проверь UI-валидацию, одним `reduce` отобрази ошибки и не вызывай use case при
|
|
14
|
+
невалидной форме. Domain-валидация остаётся внутри use case/domain слоя.
|
|
15
|
+
- Для двух и более значений передавай вложенный `{UseCase}.Params`, а не `Pair`, `Triple` или map.
|
|
16
|
+
- При submit сохрани запущенный `Job` в Model и сбрось его через `invokeOnCompletion`; состояние
|
|
17
|
+
кнопки выводи из `isSubmitting`. Успех, требующий навигации/snackbar, отправляй через Event;
|
|
18
|
+
ожидаемые ошибки обрабатывай в `catch`.
|
|
19
|
+
- Если форма сначала загружает данные, дополнительно используй network-only или Room-backed
|
|
20
|
+
reference только для этого потока и не смешивай load intent с submit intent.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Network-only screen
|
|
2
|
+
|
|
3
|
+
Используй этот режим, когда результат запроса нужен только текущему экрану и сохранять его в Room
|
|
4
|
+
не требуется.
|
|
5
|
+
|
|
6
|
+
## Контракт
|
|
7
|
+
|
|
8
|
+
- Добавь `LoadData` или семантический одноразовый intent, поле результата и nullable `Job` запроса в
|
|
9
|
+
Model. Вычисляй loading из активности этого `Job`; не храни отдельный изменяемый boolean.
|
|
10
|
+
- В `init` отправляй load intent, если запрос должен выполняться при создании ViewModel. Не дублируй
|
|
11
|
+
запуск через `LaunchedEffect` в Screen.
|
|
12
|
+
- В ветке intent запусти `Job`, сохрани его в Model, сбрось через `invokeOnCompletion`, вызови use
|
|
13
|
+
case через `.getOrThrow()` и сохрани успешный результат через `reduce`.
|
|
14
|
+
- Обработай ожидаемые domain/network исключения в `catch` согласно соседним ViewModel. Не храни
|
|
15
|
+
`Result` в Model.
|
|
16
|
+
- Для retry используй тот же load intent; отдельный intent нужен только если пользовательское
|
|
17
|
+
действие имеет отличную семантику.
|
|
18
|
+
|
|
19
|
+
Если сетевой контракт или use case отсутствует, создай их через `create-ktor-endpoint` и
|
|
20
|
+
`create-usecase`. Не добавляй Room только для унификации с другими экранами.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Paginated screen
|
|
2
|
+
|
|
3
|
+
Используй этот режим, когда экран получает `Flow<PagingData<Item>>` и отображает Paging load states.
|
|
4
|
+
|
|
5
|
+
## ViewModel
|
|
6
|
+
|
|
7
|
+
- Предпочитай готовый paging use case. Публичный `val {items}PagingFlow` может быть исключением из
|
|
8
|
+
правила «всё состояние в Model»: это immutable data stream, а не вручную изменяемое UI-состояние.
|
|
9
|
+
- Заверши paging flow через `cachedIn(this)` либо эквивалент, уже принятый базовым ViewModel проекта.
|
|
10
|
+
- Если запрос зависит от фильтра/поиска, храни параметры в Model и строй flow как
|
|
11
|
+
`stateFlow.map { ... }.distinctUntilChanged().flatMapLatest { pagingUseCase(it) }.cachedIn(this)`.
|
|
12
|
+
Не держи второй mutable-набор тех же параметров.
|
|
13
|
+
- Состояния toolbar, selection и pull-to-refresh остаются в Model.
|
|
14
|
+
|
|
15
|
+
## Screen
|
|
16
|
+
|
|
17
|
+
- В публичном Screen вызови `collectAsLazyPagingItems()` и передай результат в content отдельно от
|
|
18
|
+
Model.
|
|
19
|
+
- Рендери initial loading, append loading, empty и error по `loadState`, не дублируя их вручную в
|
|
20
|
+
Model.
|
|
21
|
+
- Retry и refresh начинаются с отдельных Intent. ViewModel отправляет одноразовый Event, публичный
|
|
22
|
+
Screen обрабатывает его через `ObserveAsEvents` и вызывает `pagingItems.retry()` или
|
|
23
|
+
`pagingItems.refresh()`; content только диспатчит Intent. После MVI-coordinated refresh отправь
|
|
24
|
+
завершающий intent для сброса `isRefreshing`, если Model хранит это UI-состояние.
|
|
25
|
+
- Preview создаёт `PagingData.from(items)` через локальный Flow и передаёт полученные
|
|
26
|
+
`LazyPagingItems` в content.
|
|
27
|
+
|
|
28
|
+
Этот skill оформляет только экран. `PagingSource`, `Pager`, `RemoteMediator`, DAO и API относятся к
|
|
29
|
+
data/domain-потоку и должны быть готовы заранее либо созданы через `create-paging-flow`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Room-backed collect/load screen
|
|
2
|
+
|
|
3
|
+
Используй этот режим, когда Room является источником отображаемых данных, а сеть обновляет Room.
|
|
4
|
+
|
|
5
|
+
## Контракт
|
|
6
|
+
|
|
7
|
+
- Создай отдельные intent-ы `Collect{Feature}` и `Load{Feature}`. Не объединяй бесконечное
|
|
8
|
+
наблюдение и одноразовое обновление в одну ветку.
|
|
9
|
+
- В `init` сначала запускай collect intent, затем load intent, если экран должен автоматически
|
|
10
|
+
обновиться из сети.
|
|
11
|
+
- В collect-ветке собирай `FlowUseCase` и обновляй Model через `reduce` при каждом значении.
|
|
12
|
+
- Для сетевого обновления храни nullable request-`Job` в Model и вычисляй loading из его активности.
|
|
13
|
+
В load-ветке сохрани запущенный `Job`, сбрось его через `invokeOnCompletion` и вызови одноразовый
|
|
14
|
+
use case через `.getOrThrow()`.
|
|
15
|
+
- Не копируй сетевой response в Model: load use case записывает Room, а collect-ветка доставляет
|
|
16
|
+
новое состояние.
|
|
17
|
+
- Pull-to-refresh повторно отправляет load intent либо отдельный refresh intent только при
|
|
18
|
+
необходимости собственного UI-состояния.
|
|
19
|
+
- Loading/empty вычисляй из реального состояния фичи; не добавляй `isLoading`, если пустая Room-
|
|
20
|
+
коллекция уже однозначно задаёт initial state.
|
|
21
|
+
|
|
22
|
+
Если поток данных отсутствует, собери его через `create-data-layer` с вариантом
|
|
23
|
+
Ktor → mapper → Room. Screen зависит только от готовых `UseCase` / `FlowUseCase`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Static screen
|
|
2
|
+
|
|
3
|
+
Используй этот режим, когда экран не загружает и не наблюдает данные.
|
|
4
|
+
|
|
5
|
+
- Не добавляй искусственные `LoadData`, `isLoading`, use case или `init` только ради шаблона.
|
|
6
|
+
- Model содержит только состояние, которое реально меняется: выбранную вкладку, раскрытую секцию,
|
|
7
|
+
локальный toggle и подобное.
|
|
8
|
+
- Intent нужен только для реальных действий. Навигационное действие отправляет Event через
|
|
9
|
+
`send(...)`, а публичный Screen обрабатывает его через `ObserveAsEvents`.
|
|
10
|
+
- Даже если экран полностью детерминирован входными аргументами, создай обязательные `ViewModel`,
|
|
11
|
+
`Model` и `Intent` по MVI-контракту проекта, но не добавляй в них фиктивную загрузку или состояние.
|
|
12
|
+
- Preview должен показывать содержательные визуальные варианты, а не loading/content, которого у
|
|
13
|
+
экрана нет.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-guide
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит создать практический Android-гайд по конкретному API или теме,
|
|
5
|
+
который публикуется как связанная пара артефактов: рабочий публичный GitHub-репозиторий и
|
|
6
|
+
объясняющая страница в Notion — например "создай гайд по Navigation3", "сделай гайд про
|
|
7
|
+
EyeDropper", "напиши гайд по Paging 3 с RemoteMediator". Проводит исследование по первичным
|
|
8
|
+
источникам, создаёт и проверяет Android-проект из актуального `MyApplication`, пишет или
|
|
9
|
+
обновляет страницу Notion в data source `POSTS` и публикует оба артефакта. Не используй для
|
|
10
|
+
обычной фичи, экрана или доработки в уже существующем прикладном проекте — для этого есть
|
|
11
|
+
`create-feature-scaffold-screen` и другие `create-*` skills. Не используй, если нужен только
|
|
12
|
+
пустой Android-проект без исследования и без Notion-страницы — для этого есть
|
|
13
|
+
`create-project-from-template`.
|
|
14
|
+
metadata:
|
|
15
|
+
author: michaelbel
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Создание гайда: Android-проект + Notion-страница
|
|
19
|
+
|
|
20
|
+
Каждый гайд состоит из двух связанных артефактов: рабочего Android-проекта в отдельном публичном
|
|
21
|
+
GitHub-репозитории и страницы в Notion, которая объясняет API и ссылается на точные файлы проекта.
|
|
22
|
+
Если страница уже существует, обновляй только её и не переноси между базами. Если страницы нет, по
|
|
23
|
+
умолчанию создавай её в data source `POSTS`, если пользователь явно не указал другое место.
|
|
24
|
+
|
|
25
|
+
Это единственное место, где живёт эта функциональность. Не создавай и не поддерживай для неё
|
|
26
|
+
отдельный репозиторий или второй skill.
|
|
27
|
+
|
|
28
|
+
## Обязательный контекст
|
|
29
|
+
|
|
30
|
+
Перед работой:
|
|
31
|
+
|
|
32
|
+
1. прочитай [references/guide-defaults.yaml](references/guide-defaults.yaml) — личные дефолты
|
|
33
|
+
(owner, package prefix, шаблон `MyApplication`, data source Notion `POSTS`);
|
|
34
|
+
2. прочитай [references/existing-guides.yaml](references/existing-guides.yaml) как каталог уже
|
|
35
|
+
изданных гайдов — не создавай дубликат уже изданной темы, обнови существующий репозиторий и
|
|
36
|
+
страницу вместо этого;
|
|
37
|
+
3. загрузи актуальные Kotlin/Compose/Git/GitHub правила из `michaelbel/cuckcoder` через MCP
|
|
38
|
+
`cuckcoder`;
|
|
39
|
+
4. изучи актуальную ветку `michaelbel/MyApplication` перед созданием Android-проекта.
|
|
40
|
+
|
|
41
|
+
`cuckcoder` определяет общий Kotlin, Compose, Git и GitHub-стиль. Этот skill определяет процесс
|
|
42
|
+
создания гайда. Если правила конфликтуют именно в вопросах исследования, структуры учебного
|
|
43
|
+
проекта, публикации или валидации гайда, применяй этот skill.
|
|
44
|
+
|
|
45
|
+
## Команда пользователя
|
|
46
|
+
|
|
47
|
+
Минимальный запрос:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
Создай гайд по Navigation3
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Не заставляй пользователя заполнять анкету. Сам выбери значения из
|
|
54
|
+
[references/guide-defaults.yaml](references/guide-defaults.yaml) и зафиксируй их в манифесте.
|
|
55
|
+
Задавай вопрос только тогда, когда неоднозначность заметно меняет тему, объём или публичный
|
|
56
|
+
результат.
|
|
57
|
+
|
|
58
|
+
## Порядок работы
|
|
59
|
+
|
|
60
|
+
### 1. Определи границы темы
|
|
61
|
+
|
|
62
|
+
Зафиксируй прямо в этом контексте:
|
|
63
|
+
|
|
64
|
+
- основной API;
|
|
65
|
+
- связанные компоненты;
|
|
66
|
+
- статус API: stable, alpha, experimental или platform;
|
|
67
|
+
- минимальную версию Android и библиотек;
|
|
68
|
+
- вопросы, которые должен закрыть гайд;
|
|
69
|
+
- подходящий тип проекта: `catalog`, `scenario` или `single` (см.
|
|
70
|
+
[references/android-project.md](references/android-project.md)).
|
|
71
|
+
|
|
72
|
+
### 2. Проведи исследование
|
|
73
|
+
|
|
74
|
+
Приоритет источников — [references/research.md](references/research.md):
|
|
75
|
+
|
|
76
|
+
1. официальная документация;
|
|
77
|
+
2. исходный код AndroidX или Android Open Source Project;
|
|
78
|
+
3. официальные samples;
|
|
79
|
+
4. release notes;
|
|
80
|
+
5. issue tracker;
|
|
81
|
+
6. сторонние статьи только как дополнительный контекст.
|
|
82
|
+
|
|
83
|
+
Для AndroidX зафиксируй точный ref или commit SHA и пути к изученным файлам (правила атрибуции —
|
|
84
|
+
[references/androidx.md](references/androidx.md)). Не считай существующий гайд из
|
|
85
|
+
`existing-guides.yaml` техническим источником истины.
|
|
86
|
+
|
|
87
|
+
Создай директорию `~/Projects/<Topic>` (PascalCase, sibling к `MyApplication` и остальным гайдам)
|
|
88
|
+
и в ней рабочие файлы:
|
|
89
|
+
|
|
90
|
+
- `.guidekit/manifest.yaml` — из [templates/guide-manifest.yaml](templates/guide-manifest.yaml);
|
|
91
|
+
- `.guidekit/research.md` — по структуре из
|
|
92
|
+
[references/research.md](references/research.md#research-report);
|
|
93
|
+
- `.guidekit/implementation-plan.md` — из
|
|
94
|
+
[templates/implementation-plan.md](templates/implementation-plan.md).
|
|
95
|
+
|
|
96
|
+
Сначала заполни манифест и план, затем переходи к реализации.
|
|
97
|
+
|
|
98
|
+
### 3. Реализуй и провалидируй Android-проект
|
|
99
|
+
|
|
100
|
+
Делегируй фоновому агенту, чтобы Gradle-логи и весь код не засоряли этот контекст:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Agent(subagent_type: "guide-android-builder")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Передай в промпте: полный `.guidekit/manifest.yaml`, `.guidekit/implementation-plan.md` и
|
|
107
|
+
абсолютный путь `~/Projects/<Topic>`. Этот агент сам вызовет skill `create-project-from-template`
|
|
108
|
+
для scaffold из актуального `MyApplication`, реализует сценарии и прогонит обязательные Gradle-
|
|
109
|
+
проверки. Не публикуй и не пиши Notion-страницу, пока этот агент не вернул успешный результат.
|
|
110
|
+
|
|
111
|
+
### 4. Напиши страницу Notion
|
|
112
|
+
|
|
113
|
+
Только после успешной валидации Android-проекта делегируй:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Agent(subagent_type: "guide-writer")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Передай в промпте: `.guidekit/manifest.yaml`, путь/URL репозитория, реализованные сценарии и
|
|
120
|
+
краткий summary от `guide-android-builder`. Страница пишется только на основе проверенного
|
|
121
|
+
проекта — примеры кода, имена классов, версии, package и ссылки должны совпадать с репозиторием.
|
|
122
|
+
|
|
123
|
+
### 5. Проведи перекрёстную проверку
|
|
124
|
+
|
|
125
|
+
В этом контексте, по [references/validation.md](references/validation.md), сверь:
|
|
126
|
+
|
|
127
|
+
- все заявленные сценарии реализованы;
|
|
128
|
+
- каждый путь из Notion существует в GitHub;
|
|
129
|
+
- каждый большой фрагмент кода в Notion совпадает с проектом;
|
|
130
|
+
- версии зависимостей совпадают;
|
|
131
|
+
- experimental API помечены;
|
|
132
|
+
- заимствования из AndroidX атрибутированы;
|
|
133
|
+
- ссылки открываются;
|
|
134
|
+
- README и Notion не противоречат друг другу.
|
|
135
|
+
|
|
136
|
+
### 6. Опубликуй
|
|
137
|
+
|
|
138
|
+
Порядок публикации — [references/github.md](references/github.md):
|
|
139
|
+
|
|
140
|
+
1. создай публичный GitHub-репозиторий (`gh repo create`, owner `michaelbel`) и запушь
|
|
141
|
+
проверенный проект;
|
|
142
|
+
2. обнови существующую страницу Notion или подтверди новую в data source `POSTS`;
|
|
143
|
+
3. добавь в Notion ссылки на репозиторий и точные samples;
|
|
144
|
+
4. при необходимости добавь ссылку на Notion в README;
|
|
145
|
+
5. запусти skill `github-repo-settings` для приведения репозитория к стандартному состоянию
|
|
146
|
+
(description, topics, disabled Wikis/Issues/Discussions/Projects/PRs, Sponsorships);
|
|
147
|
+
6. повторно проверь публичные URL.
|
|
148
|
+
|
|
149
|
+
Не останавливайся перед этим шагом для отдельного подтверждения — публикация автономна, но
|
|
150
|
+
финальный отчёт обязан явно перечислить, что было опубликовано.
|
|
151
|
+
|
|
152
|
+
## Обязательный финальный отчёт
|
|
153
|
+
|
|
154
|
+
Полный формат — [references/output-contract.md](references/output-contract.md). Кратко: URL
|
|
155
|
+
GitHub-репозитория, URL страницы Notion, тип проекта, версии ключевых библиотек, список
|
|
156
|
+
реализованных сценариев, результаты Gradle-проверок, основные первичные источники, известные
|
|
157
|
+
ограничения. Не пиши «успешно», если проверка не запускалась.
|
|
158
|
+
|
|
159
|
+
## Запреты
|
|
160
|
+
|
|
161
|
+
- Не выдумывай API, версии, параметры и поведение.
|
|
162
|
+
- Не вставляй в Notion код, которого нет в проекте.
|
|
163
|
+
- Не копируй AndroidX-код без фиксации происхождения и лицензии.
|
|
164
|
+
- Не добавляй Clean Architecture, DI, многомодульность, Room или сеть без необходимости.
|
|
165
|
+
- Не публикуй черновой проект вместо рабочего примера.
|
|
166
|
+
- Не заменяй выбранный API альтернативной библиотекой без согласования.
|
|
167
|
+
- Не сохраняй токены, Notion IDs, пароли и другие секреты в Git.
|
|
168
|
+
- Не создавай дубликат уже изданной темы из `existing-guides.yaml` — обновляй существующий
|
|
169
|
+
репозиторий и страницу.
|