@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,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-datastore-preference
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит добавить или изменить типизированную настройку, флаг, идентификатор
|
|
5
|
+
или timestamp в Preferences DataStore, предоставить для него read/write/Flow use case либо
|
|
6
|
+
определить очистку при logout, смене пользователя или reset. Не используй для коллекций,
|
|
7
|
+
связанных сущностей, очередей операций и данных, которым нужны запросы или транзакции Room; для
|
|
8
|
+
них нужен Room-backed workflow. Не используй для полного экрана, который лишь потребляет уже
|
|
9
|
+
существующую настройку.
|
|
10
|
+
metadata:
|
|
11
|
+
author: michaelbel
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Новая настройка DataStore
|
|
15
|
+
|
|
16
|
+
Добавляет минимальный законченный поток для небольшого сохраняемого значения: типизированный ключ,
|
|
17
|
+
доступ через существующий DataStore-класс, domain use case и явно выбранный жизненный цикл очистки.
|
|
18
|
+
Сохраняй принятую в целевом проекте структуру `PreferenceKey`, `SettingsDataStore` и DI; не создавай
|
|
19
|
+
вторую обёртку или новый DataStore-файл для одной настройки.
|
|
20
|
+
|
|
21
|
+
## Определи контракт
|
|
22
|
+
|
|
23
|
+
До правок зафиксируй:
|
|
24
|
+
|
|
25
|
+
- Kotlin-тип и допустимое отсутствие значения;
|
|
26
|
+
- семантику отсутствия: `null`, domain default или «ещё не задавалось»;
|
|
27
|
+
- нужен ли разовый read, наблюдаемый `Flow`, write, remove или несколько этих операций;
|
|
28
|
+
- срок жизни: установка приложения, устройство, авторизованная сессия, конкретный пользователь или
|
|
29
|
+
временное решение фичи;
|
|
30
|
+
- при каких событиях значение очищается: logout, смена аккаунта, сброс настроек или никогда.
|
|
31
|
+
|
|
32
|
+
Не подменяй отсутствующее значение дефолтом в низкоуровневом DataStore-классе, если `null` несёт
|
|
33
|
+
domain-смысл. Выбирай default в конкретном use case, которому известна бизнес-семантика.
|
|
34
|
+
|
|
35
|
+
Для account-scoped значения выбери один контракт явно:
|
|
36
|
+
|
|
37
|
+
- session-only: храни owner id и value атомарно, считай несовпадение owner отсутствующим значением и
|
|
38
|
+
очищай пару при account switch;
|
|
39
|
+
- remembered per-account: используй безопасно namespaced key только для малого ограниченного набора
|
|
40
|
+
аккаунтов; для неограниченного множества и запросов по владельцу используй Room.
|
|
41
|
+
|
|
42
|
+
Один общий value key без owner недопустим: он может кратковременно отдать значение предыдущего
|
|
43
|
+
пользователя после смены аккаунта.
|
|
44
|
+
|
|
45
|
+
## Получи законченный результат
|
|
46
|
+
|
|
47
|
+
1. Добавь один типизированный `PreferenceKey<T>` с поддерживаемым Preferences DataStore типом.
|
|
48
|
+
Строковое имя ключа является persisted schema: для существующего значения сохраняй его буквально,
|
|
49
|
+
не переименовывай ради нового Kotlin-имени без явной миграции.
|
|
50
|
+
2. Используй существующие generic `getValue`, `getValueFlow`, `setValue` и `removeValue`, если они уже
|
|
51
|
+
покрывают операцию. Расширяй `SettingsDataStore` только когда требуется новая атомарная операция,
|
|
52
|
+
а не ради feature-specific метода.
|
|
53
|
+
3. Размести чтение, запись и бизнес-решения в конкретных `UseCase` / `FlowUseCase` из
|
|
54
|
+
`shared/domain/usecase`; внедри `SettingsDataStore` напрямую. UI и ViewModel не должны знать
|
|
55
|
+
DataStore key или обращаться к DataStore напрямую.
|
|
56
|
+
4. Для read-modify-write нескольких значений проверь, требуется ли одна атомарная `edit`-транзакция.
|
|
57
|
+
Не собирай несколько независимых `setValue` в use case, если наблюдатель не должен видеть
|
|
58
|
+
промежуточное состояние.
|
|
59
|
+
5. Подключи удаление к владельцу lifecycle-события. На logout очищай session-only значения, но не
|
|
60
|
+
удаляй remembered per-account и device-scoped настройки. При смене пользователя сначала смени
|
|
61
|
+
owner scope, затем разрешай чтение account-scoped значения.
|
|
62
|
+
|
|
63
|
+
## Границы реализации
|
|
64
|
+
|
|
65
|
+
- Используй разовый suspend read для решения, принимаемого один раз, и `FlowUseCase` для состояния,
|
|
66
|
+
которое должно обновлять потребителя после записи.
|
|
67
|
+
- Не вводи blocking read в новую логику. Сохраняй существующий blocking API только там, где
|
|
68
|
+
синхронную инициализацию уже требует инфраструктура приложения.
|
|
69
|
+
- Не сериализуй коллекцию или сложный изменяемый объект в строку только для обхода Room. Допускай
|
|
70
|
+
кодированное значение лишь для небольшого стабильного типа и только если такой формат уже принят
|
|
71
|
+
проектом.
|
|
72
|
+
- Не создавай Repository или Interactor и не переносись на Proto DataStore без отдельного запроса.
|
|
73
|
+
- Если несколько ключей разных типов очищаются вместе, операция очистки должна поддерживать их без
|
|
74
|
+
небезопасного общего generic-типа.
|
|
75
|
+
|
|
76
|
+
## Проверка результата
|
|
77
|
+
|
|
78
|
+
Проверь cold start без значения, чтение после записи, обновление Flow, удаление на каждом выбранном
|
|
79
|
+
lifecycle-событии и сохранение ключей другого срока жизни. Для существующего ключа отдельно проверь,
|
|
80
|
+
что строковое persisted-имя не изменилось.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-domain-mapper
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит создать или изменить mapper между network, Room и domain-моделями
|
|
5
|
+
либо KTX-функции `entity`/`entities`. Не используй для создания самих API-моделей, DAO, endpoint
|
|
6
|
+
или бизнес-операции use case.
|
|
7
|
+
metadata:
|
|
8
|
+
author: michaelbel
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Создание domain-mapper
|
|
12
|
+
|
|
13
|
+
Изучи соседние `shared/domain/mapper/*Ktx.kt` и помести преобразование в тематический KTX-файл. Не
|
|
14
|
+
создавай stateful `Mapper`-класс и не переноси mapping во ViewModel или use case.
|
|
15
|
+
|
|
16
|
+
## Контракт
|
|
17
|
+
|
|
18
|
+
- Называй `Response -> Entity` extension-функцию `entity`, а преобразование списка — `entities`.
|
|
19
|
+
- Для другого направления используй короткое имя целевого типа или существующее соглашение проекта.
|
|
20
|
+
- Размещай параметры функции и аргументы конструктора целевой модели по одному на строку.
|
|
21
|
+
- Передавай внешние значения, которых нет в source-модели, явными параметрами mapper; не читай
|
|
22
|
+
состояние из service locator или singleton.
|
|
23
|
+
- Возвращай non-null entity. Nullable поля ответа заполняй через `orEmpty`, включая nullable
|
|
24
|
+
примитивы из `PrimitiveKtx.kt`; добавь недостающее primitive-extension туда.
|
|
25
|
+
- Не используй `return null` или `mapNotNull` для молчаливого удаления некорректных ответов. Если
|
|
26
|
+
значение нельзя корректно восстановить, оставь валидацию и domain-ошибку вызывающей операции.
|
|
27
|
+
- Не создавай отдельную UI-модель, если entity/domain-модель уже выражает нужное состояние.
|
|
28
|
+
|
|
29
|
+
Добавь focused unit-тест mapper, когда преобразование содержит ветвление, вложенные коллекции,
|
|
30
|
+
форматирование или нетривиальные default-значения.
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-feature-alert-dialog
|
|
3
|
+
description: >-
|
|
4
|
+
Use when пользователь просит создать в Compose диалог подтверждения, alert dialog, picker dialog
|
|
5
|
+
или простой status popup с явным действием confirm/dismiss, либо говорит "create a dialog", "add
|
|
6
|
+
an alert dialog", "confirmation popup", "new AlertDialog". Строит `AlertDialog` с одной или двумя
|
|
7
|
+
кнопками для подтверждающих сценариев и `BasicAlertDialog` — для пикера без кнопок действия. Если
|
|
8
|
+
нужна прокручиваемая модальная поверхность, закреплённая снизу экрана, используй
|
|
9
|
+
[create-feature-bottom-sheet](../create-feature-bottom-sheet/SKILL.md) вместо этого скилла. Для
|
|
10
|
+
полного экрана используй
|
|
11
|
+
[create-feature-scaffold-screen](../create-feature-scaffold-screen/SKILL.md), для простого
|
|
12
|
+
переиспользуемого компонента без диалоговой семантики —
|
|
13
|
+
[create-shared-component](../create-shared-component/SKILL.md).
|
|
14
|
+
metadata:
|
|
15
|
+
author: michaelbel
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Новый Alert Dialog
|
|
19
|
+
|
|
20
|
+
Требует зависимость `androidx.compose.material3:material3`.
|
|
21
|
+
|
|
22
|
+
Создаёт Compose-диалог проекта. Замени `{Feature}` на назначение диалога, `{feature}` на имя в
|
|
23
|
+
lower camel case, а `{package}` на целевой пакет.
|
|
24
|
+
|
|
25
|
+
Используй `AlertDialog`, когда в диалоге есть кнопки действий (с текстом, иконкой или списком в
|
|
26
|
+
слоте `text`). Используй `BasicAlertDialog`, когда кнопок действий нет.
|
|
27
|
+
|
|
28
|
+
## Фаза 1: создать папку фичи
|
|
29
|
+
|
|
30
|
+
Создай папку `features/{feature}_dialog` — постфикс `_dialog` обязателен, даже если фича сама по
|
|
31
|
+
себе не заканчивается на «диалог».
|
|
32
|
+
|
|
33
|
+
## Фаза 2: Intent и Model
|
|
34
|
+
|
|
35
|
+
Внутри `features/{feature}_dialog` создай папку `intent`. Папку `model` создавай только когда
|
|
36
|
+
диалогу нужны отображаемые данные; если данных нет, не создавай ни папку, ни файл Model.
|
|
37
|
+
|
|
38
|
+
Файлы:
|
|
39
|
+
- `features/{feature}_dialog/{Feature}Dialog.kt`
|
|
40
|
+
- `features/{feature}_dialog/intent/{Feature}Intent.kt`
|
|
41
|
+
- optional `features/{feature}_dialog/model/{Feature}Model.kt`
|
|
42
|
+
|
|
43
|
+
### {Feature}Intent.kt
|
|
44
|
+
|
|
45
|
+
```kotlin
|
|
46
|
+
package {package}.features.{feature}_dialog.intent
|
|
47
|
+
|
|
48
|
+
import {package}.shared.mvi.Intent
|
|
49
|
+
|
|
50
|
+
sealed interface {Feature}Intent: Intent {
|
|
51
|
+
data object DismissClick: {Feature}Intent
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Правила:
|
|
56
|
+
- `DismissClick` присутствует всегда.
|
|
57
|
+
- Если у диалога есть кнопка подтверждения, добавь `data object ConfirmClick: {Feature}Intent`.
|
|
58
|
+
- Для остальных действий (выбор опции, клик по элементу и т. д.) добавляй свои `data object` /
|
|
59
|
+
`data class` записи; `data object` всегда идёт перед `data class`.
|
|
60
|
+
|
|
61
|
+
### {Feature}Model.kt (только если есть данные)
|
|
62
|
+
|
|
63
|
+
```kotlin
|
|
64
|
+
package {package}.features.{feature}_dialog.model
|
|
65
|
+
|
|
66
|
+
import {package}.shared.mvi.Model
|
|
67
|
+
|
|
68
|
+
data class {Feature}Model(
|
|
69
|
+
val selectedOptionId: String = ""
|
|
70
|
+
): Model
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## AlertDialog с заголовком и двумя кнопками (без Model)
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
package {package}.features.{feature}_dialog
|
|
77
|
+
|
|
78
|
+
// Добавь все необходимые импорты
|
|
79
|
+
|
|
80
|
+
@Composable
|
|
81
|
+
fun {Feature}Dialog(
|
|
82
|
+
dispatch: ({Feature}Intent) -> Unit
|
|
83
|
+
) {
|
|
84
|
+
AlertDialog(
|
|
85
|
+
onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
|
|
86
|
+
confirmButton = {
|
|
87
|
+
TextButton(
|
|
88
|
+
onClick = {
|
|
89
|
+
dispatch({Feature}Intent.ConfirmClick)
|
|
90
|
+
dispatch({Feature}Intent.DismissClick)
|
|
91
|
+
}
|
|
92
|
+
) {
|
|
93
|
+
Text(
|
|
94
|
+
text = stringResource(AppStrings.{Feature}Confirm),
|
|
95
|
+
style = MaterialTheme.typography.medium14
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
dismissButton = {
|
|
100
|
+
TextButton(
|
|
101
|
+
onClick = { dispatch({Feature}Intent.DismissClick) }
|
|
102
|
+
) {
|
|
103
|
+
Text(
|
|
104
|
+
text = stringResource(AppStrings.DialogCancel),
|
|
105
|
+
style = MaterialTheme.typography.regular14
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
title = {
|
|
110
|
+
Text(
|
|
111
|
+
text = stringResource(AppStrings.{Feature}Title),
|
|
112
|
+
style = MaterialTheme.typography.regular22.copy(
|
|
113
|
+
lineHeight = 22.sp
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
text = {
|
|
118
|
+
Text(
|
|
119
|
+
text = stringResource(AppStrings.{Feature}Message),
|
|
120
|
+
style = MaterialTheme.typography.regular14
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
titleContentColor = MaterialTheme.colorScheme.onBackground,
|
|
124
|
+
textContentColor = MaterialTheme.colorScheme.onSurfaceVariant2
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@PreviewWrapper(ThemeWrapper::class)
|
|
129
|
+
@Preview
|
|
130
|
+
@Composable
|
|
131
|
+
private fun {Feature}DialogPreview() {
|
|
132
|
+
Box(
|
|
133
|
+
modifier = Modifier.fillMaxSize()
|
|
134
|
+
) {
|
|
135
|
+
{Feature}Dialog(
|
|
136
|
+
dispatch = {}
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## AlertDialog с одной кнопкой (иконка опциональна)
|
|
143
|
+
|
|
144
|
+
```kotlin
|
|
145
|
+
package {package}.features.{feature}_dialog
|
|
146
|
+
|
|
147
|
+
// Добавь все необходимые импорты
|
|
148
|
+
|
|
149
|
+
@Composable
|
|
150
|
+
fun {Feature}Dialog(
|
|
151
|
+
dispatch: ({Feature}Intent) -> Unit
|
|
152
|
+
) {
|
|
153
|
+
AlertDialog(
|
|
154
|
+
onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
|
|
155
|
+
confirmButton = {
|
|
156
|
+
TextButton(
|
|
157
|
+
onClick = { dispatch({Feature}Intent.DismissClick) }
|
|
158
|
+
) {
|
|
159
|
+
Text(
|
|
160
|
+
text = stringResource(AppStrings.DialogOk),
|
|
161
|
+
style = MaterialTheme.typography.medium14
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
icon = {
|
|
166
|
+
Icon(
|
|
167
|
+
imageVector = {IconName},
|
|
168
|
+
contentDescription = null,
|
|
169
|
+
modifier = Modifier.size(44.dp)
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
title = {
|
|
173
|
+
Text(
|
|
174
|
+
text = stringResource(AppStrings.{Feature}Title),
|
|
175
|
+
style = MaterialTheme.typography.regular22
|
|
176
|
+
)
|
|
177
|
+
},
|
|
178
|
+
text = {
|
|
179
|
+
Text(
|
|
180
|
+
text = stringResource(AppStrings.{Feature}Message),
|
|
181
|
+
style = MaterialTheme.typography.regular14
|
|
182
|
+
)
|
|
183
|
+
},
|
|
184
|
+
iconContentColor = MaterialTheme.colorScheme.error,
|
|
185
|
+
titleContentColor = MaterialTheme.colorScheme.error,
|
|
186
|
+
textContentColor = MaterialTheme.colorScheme.onBackground
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@PreviewWrapper(ThemeWrapper::class)
|
|
191
|
+
@Preview
|
|
192
|
+
@Composable
|
|
193
|
+
private fun {Feature}DialogPreview() {
|
|
194
|
+
Box(
|
|
195
|
+
modifier = Modifier.fillMaxSize()
|
|
196
|
+
) {
|
|
197
|
+
{Feature}Dialog(
|
|
198
|
+
dispatch = {}
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Убери блок `icon` целиком и `iconContentColor`, если иконка не нужна; тогда `titleContentColor` /
|
|
205
|
+
`textContentColor` возвращаются к обычным `onBackground` / `onSurfaceVariant2`. Для диалогов
|
|
206
|
+
ошибок/предупреждений задавай `iconContentColor` и `titleContentColor` в
|
|
207
|
+
`MaterialTheme.colorScheme.error`.
|
|
208
|
+
|
|
209
|
+
## AlertDialog со списком в тексте (с Model)
|
|
210
|
+
|
|
211
|
+
Когда нужен диалог с заголовком, кнопками и выбираемым списком, размещай список внутри слота
|
|
212
|
+
`text`, а не переключайся на `BasicAlertDialog`.
|
|
213
|
+
|
|
214
|
+
```kotlin
|
|
215
|
+
package {package}.features.{feature}_dialog
|
|
216
|
+
|
|
217
|
+
// Добавь все необходимые импорты
|
|
218
|
+
|
|
219
|
+
@Composable
|
|
220
|
+
fun {Feature}Dialog(
|
|
221
|
+
state: {Feature}Model,
|
|
222
|
+
dispatch: ({Feature}Intent) -> Unit
|
|
223
|
+
) {
|
|
224
|
+
AlertDialog(
|
|
225
|
+
onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
|
|
226
|
+
confirmButton = {
|
|
227
|
+
TextButton(
|
|
228
|
+
onClick = {
|
|
229
|
+
dispatch({Feature}Intent.ConfirmClick)
|
|
230
|
+
dispatch({Feature}Intent.DismissClick)
|
|
231
|
+
},
|
|
232
|
+
enabled = state.selectedOptionId.isNotEmpty()
|
|
233
|
+
) {
|
|
234
|
+
Text(
|
|
235
|
+
text = stringResource(AppStrings.DialogChoose),
|
|
236
|
+
style = MaterialTheme.typography.medium14
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
dismissButton = {
|
|
241
|
+
TextButton(
|
|
242
|
+
onClick = { dispatch({Feature}Intent.DismissClick) }
|
|
243
|
+
) {
|
|
244
|
+
Text(
|
|
245
|
+
text = stringResource(AppStrings.DialogCancel),
|
|
246
|
+
style = MaterialTheme.typography.regular14
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
text = {
|
|
251
|
+
Column {
|
|
252
|
+
Text(
|
|
253
|
+
text = stringResource(AppStrings.{Feature}Title),
|
|
254
|
+
style = MaterialTheme.typography.regular22
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
Spacer(
|
|
258
|
+
modifier = Modifier.height(24.dp)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
state.options.forEach { option ->
|
|
262
|
+
Row(
|
|
263
|
+
modifier = Modifier
|
|
264
|
+
.fillMaxWidth()
|
|
265
|
+
.height(56.dp)
|
|
266
|
+
.clickable { dispatch({Feature}Intent.OptionClick(option.id)) },
|
|
267
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
268
|
+
verticalAlignment = Alignment.CenterVertically
|
|
269
|
+
) {
|
|
270
|
+
Text(
|
|
271
|
+
text = option.title,
|
|
272
|
+
style = MaterialTheme.typography.regular14
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
RadioButton(
|
|
276
|
+
selected = state.selectedOptionId == option.id,
|
|
277
|
+
onClick = null
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
HorizontalDivider(
|
|
282
|
+
modifier = Modifier.fillMaxWidth(),
|
|
283
|
+
color = MaterialTheme.colorScheme.outlineVariant
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
titleContentColor = MaterialTheme.colorScheme.onBackground,
|
|
289
|
+
textContentColor = MaterialTheme.colorScheme.onSurfaceVariant2
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@PreviewWrapper(ThemeWrapper::class)
|
|
294
|
+
@Preview
|
|
295
|
+
@Composable
|
|
296
|
+
private fun {Feature}DialogPreview(
|
|
297
|
+
@PreviewParameter({Feature}ModelPreviewParameterProvider::class) state: {Feature}Model
|
|
298
|
+
) {
|
|
299
|
+
Box(
|
|
300
|
+
modifier = Modifier.fillMaxSize()
|
|
301
|
+
) {
|
|
302
|
+
{Feature}Dialog(
|
|
303
|
+
state = state,
|
|
304
|
+
dispatch = {}
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private class {Feature}ModelPreviewParameterProvider: PreviewParameterProvider<{Feature}Model> {
|
|
310
|
+
private val options = listOf(
|
|
311
|
+
{Feature}Option(id = "sample-a", title = "Sample A"),
|
|
312
|
+
{Feature}Option(id = "sample-b", title = "Sample B"),
|
|
313
|
+
{Feature}Option(id = "sample-c", title = "Sample C")
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
override val values: Sequence<{Feature}Model>
|
|
317
|
+
get() = sequenceOf(
|
|
318
|
+
{Feature}Model(options = options, selectedOptionId = "sample-a"),
|
|
319
|
+
{Feature}Model(options = options, selectedOptionId = "")
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## BasicAlertDialog без кнопок
|
|
325
|
+
|
|
326
|
+
```kotlin
|
|
327
|
+
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
328
|
+
|
|
329
|
+
package {package}.features.{feature}_dialog
|
|
330
|
+
|
|
331
|
+
// Добавь все необходимые импорты
|
|
332
|
+
|
|
333
|
+
@Composable
|
|
334
|
+
fun {Feature}Dialog(
|
|
335
|
+
state: {Feature}Model,
|
|
336
|
+
dispatch: ({Feature}Intent) -> Unit
|
|
337
|
+
) {
|
|
338
|
+
BasicAlertDialog(
|
|
339
|
+
onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
|
|
340
|
+
modifier = Modifier
|
|
341
|
+
.fillMaxWidth()
|
|
342
|
+
.wrapContentHeight()
|
|
343
|
+
.clip(RoundedCornerShape(16.dp))
|
|
344
|
+
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
|
345
|
+
) {
|
|
346
|
+
Column(
|
|
347
|
+
modifier = Modifier.padding(vertical = 24.dp),
|
|
348
|
+
verticalArrangement = Arrangement.spacedBy(2.dp),
|
|
349
|
+
horizontalAlignment = Alignment.Start
|
|
350
|
+
) {
|
|
351
|
+
Text(
|
|
352
|
+
text = stringResource(AppStrings.{Feature}Title),
|
|
353
|
+
modifier = Modifier.padding(horizontal = 24.dp),
|
|
354
|
+
style = MaterialTheme.typography.titleText
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
Spacer(
|
|
358
|
+
modifier = Modifier.height(12.dp)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
state.options.forEachIndexed { index, option ->
|
|
362
|
+
SegmentedListItem(
|
|
363
|
+
modifier = Modifier
|
|
364
|
+
.padding(horizontal = 16.dp)
|
|
365
|
+
.clickable {
|
|
366
|
+
dispatch({Feature}Intent.OptionClick(option))
|
|
367
|
+
dispatch({Feature}Intent.DismissClick)
|
|
368
|
+
},
|
|
369
|
+
headlineContent = {
|
|
370
|
+
Text(
|
|
371
|
+
text = option.title,
|
|
372
|
+
style = MaterialTheme.typography.headlineText
|
|
373
|
+
)
|
|
374
|
+
},
|
|
375
|
+
trailingContent = {
|
|
376
|
+
RadioButton(
|
|
377
|
+
selected = state.selectedOption == option,
|
|
378
|
+
onClick = null
|
|
379
|
+
)
|
|
380
|
+
},
|
|
381
|
+
colors = ListItemDefaults.colors().copy(
|
|
382
|
+
containerColor = MaterialTheme.colorScheme.surface,
|
|
383
|
+
headlineColor = MaterialTheme.colorScheme.onBackground,
|
|
384
|
+
supportingTextColor = MaterialTheme.colorScheme.secondary,
|
|
385
|
+
trailingIconColor = MaterialTheme.colorScheme.onBackground
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@PreviewWrapper(ThemeWrapper::class)
|
|
394
|
+
@Preview
|
|
395
|
+
@Composable
|
|
396
|
+
private fun {Feature}Preview() {
|
|
397
|
+
Box(
|
|
398
|
+
modifier = Modifier.fillMaxSize()
|
|
399
|
+
) {
|
|
400
|
+
{Feature}Dialog(
|
|
401
|
+
state = {Feature}Model(
|
|
402
|
+
selectedOption = {Feature}Option.SampleA,
|
|
403
|
+
options = listOf(
|
|
404
|
+
{Feature}Option.SampleA,
|
|
405
|
+
{Feature}Option.SampleB,
|
|
406
|
+
{Feature}Option.SampleC
|
|
407
|
+
)
|
|
408
|
+
),
|
|
409
|
+
dispatch = {}
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Фаза 3: вызов диалога с экрана
|
|
416
|
+
|
|
417
|
+
Диалог не хранит собственный ViewModel — видимостью и данными управляет экран, который его
|
|
418
|
+
открывает.
|
|
419
|
+
|
|
420
|
+
### 1. Флаг видимости в Model экрана
|
|
421
|
+
|
|
422
|
+
В `Model` вызывающего экрана добавь `Boolean`-поле `is{Feature}DialogVisible` — `is`, имя диалога
|
|
423
|
+
(с постфиксом `Dialog`), затем `Visible`:
|
|
424
|
+
|
|
425
|
+
```kotlin
|
|
426
|
+
data class {Screen}Model(
|
|
427
|
+
val is{Feature}DialogVisible: Boolean = false
|
|
428
|
+
): Model
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 2. Показ диалога
|
|
432
|
+
|
|
433
|
+
Там, где решаешь открыть диалог, диспатчи `reduce`, выставляющий флаг в `true`:
|
|
434
|
+
|
|
435
|
+
```kotlin
|
|
436
|
+
reduce { it.copy(is{Feature}DialogVisible = true) }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### 3. Обработка intent'ов диалога в ViewModel экрана
|
|
440
|
+
|
|
441
|
+
Добавь в `{Screen}Intent` экрана один case `On{Feature}Intent`, оборачивающий весь `{Feature}Intent`
|
|
442
|
+
диалога целиком — отдельный case на каждый intent диалога не создавай:
|
|
443
|
+
|
|
444
|
+
```kotlin
|
|
445
|
+
sealed interface {Screen}Intent: Intent {
|
|
446
|
+
data class On{Feature}Intent(val intent: {Feature}Intent): {Screen}Intent
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Разбирай `intent.intent` вложенным `when` внутри `dispatch` ViewModel экрана:
|
|
451
|
+
|
|
452
|
+
```kotlin
|
|
453
|
+
override fun dispatch(intent: {Screen}Intent) {
|
|
454
|
+
when (intent) {
|
|
455
|
+
is {Screen}Intent.On{Feature}Intent -> {
|
|
456
|
+
when (intent.intent) {
|
|
457
|
+
is {Feature}Intent.DismissClick -> {
|
|
458
|
+
reduce { it.copy(is{Feature}DialogVisible = false) }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### 4. Отрисовка
|
|
467
|
+
|
|
468
|
+
Рендери диалог внутри первой, публичной функции экрана (`{Screen}Screen`, не
|
|
469
|
+
`{Screen}ScreenContent`), сразу после вызова `{Screen}ScreenContent(...)`. В `dispatch`-лямбде
|
|
470
|
+
диалога просто пробрасывай весь intent в `On{Feature}Intent`, не разбирая его case'ы в самом
|
|
471
|
+
composable:
|
|
472
|
+
|
|
473
|
+
```kotlin
|
|
474
|
+
@Composable
|
|
475
|
+
fun {Screen}Screen(
|
|
476
|
+
viewModel: {Screen}ViewModel = hiltViewModel()
|
|
477
|
+
) {
|
|
478
|
+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
|
479
|
+
|
|
480
|
+
{Screen}ScreenContent(
|
|
481
|
+
state = state,
|
|
482
|
+
dispatch = viewModel::dispatch
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if (state.is{Feature}DialogVisible) {
|
|
486
|
+
{Feature}Dialog(
|
|
487
|
+
dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}Intent(intent)) }
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Если у диалога есть Model
|
|
494
|
+
|
|
495
|
+
Когда диалог принимает `state`, не храни отдельную копию его Model в экране — вычисляй её
|
|
496
|
+
свойством `get()` из уже существующих данных экрана. Имя свойства оканчивается на `State`, а не на
|
|
497
|
+
`Model` (сам тип остаётся `{Feature}Model`):
|
|
498
|
+
|
|
499
|
+
```kotlin
|
|
500
|
+
data class {Screen}Model(
|
|
501
|
+
val {feature}Message: String = "",
|
|
502
|
+
val is{Feature}DialogVisible: Boolean = false
|
|
503
|
+
): Model {
|
|
504
|
+
val {feature}State: {Feature}Model
|
|
505
|
+
get() = {Feature}Model(
|
|
506
|
+
message = {feature}Message
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```kotlin
|
|
512
|
+
if (state.is{Feature}DialogVisible) {
|
|
513
|
+
{Feature}Dialog(
|
|
514
|
+
state = state.{feature}State,
|
|
515
|
+
dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}Intent(intent)) }
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Правила:
|
|
521
|
+
- Используй `AlertDialog`, когда в диалоге есть кнопки действий (с текстом, иконкой или списком в
|
|
522
|
+
`text`). Используй `BasicAlertDialog` только для пикера без кнопок действия.
|
|
523
|
+
- Для `BasicAlertDialog` применяй `clip` и `background` прямо на modifier диалога.
|
|
524
|
+
- На `AlertDialog` всегда указывай `titleContentColor` и `textContentColor` — обычно
|
|
525
|
+
`MaterialTheme.colorScheme.onBackground` и `MaterialTheme.colorScheme.onSurfaceVariant2`; для
|
|
526
|
+
диалогов ошибок/предупреждений — `MaterialTheme.colorScheme.error`.
|
|
527
|
+
- Sealed interface `{Feature}Intent` всегда содержит `DismissClick`; добавляй `ConfirmClick`,
|
|
528
|
+
когда есть кнопка подтверждения.
|
|
529
|
+
- Создавай `{Feature}Model` только когда диалогу нужны отображаемые данные; иначе не создавай ни
|
|
530
|
+
папку, ни файл `model`.
|
|
531
|
+
- Указывай `@file:OptIn(...)` перед `package`, когда используешь experimental API (например
|
|
532
|
+
`BasicAlertDialog` требует `ExperimentalMaterial3Api`) или любой другой opt-in, нужный коду.
|
|
533
|
+
- Делай preview каждого диалога внутри `Box(modifier = Modifier.fillMaxSize())`; `showBackground =
|
|
534
|
+
true` не указывай.
|
|
535
|
+
- Для preview с состоянием всегда создавай приватный `{Feature}ModelPreviewParameterProvider:
|
|
536
|
+
PreviewParameterProvider<{Feature}Model>` в том же файле и передавай его через
|
|
537
|
+
`@PreviewParameter`. Preview без состояния параметров не принимает.
|
|
538
|
+
- В лямбдах кнопки подтверждения диспатчи intent подтверждения перед intent закрытия.
|
|
539
|
+
- Используй анонимизированные значения для preview, такие как `0000`, `SampleA`, `SampleB` и
|
|
540
|
+
`SampleC`.
|
|
541
|
+
- Видимостью диалога управляет вызывающий экран через `Boolean`-поле `is{Feature}DialogVisible` в
|
|
542
|
+
своей `Model`; у самого диалога отдельной ViewModel нет.
|
|
543
|
+
- Экран оборачивает весь `{Feature}Intent` диалога в один case `On{Feature}Intent(val intent:
|
|
544
|
+
{Feature}Intent)` своего `{Screen}Intent`; отдельных case'ов на каждый intent диалога не
|
|
545
|
+
создавай — разбирай их вложенным `when` внутри `dispatch` ViewModel.
|
|
546
|
+
- Рендери диалог в первой, публичной функции экрана, сразу после вызова
|
|
547
|
+
`{Screen}ScreenContent(...)`.
|
|
548
|
+
- Если диалогу нужна Model, вычисляй её свойством `get()` в `Model` экрана из уже существующих
|
|
549
|
+
данных — не храни отдельную копию; имя свойства оканчивается на `State`, а не на `Model`.
|