@mrciphersmith/keryx 0.3.3 → 0.3.6
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/dist/cli.js +996 -366
- package/docs/README.md +2 -0
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +520 -48
- package/src/gdskills/bundled/stacks/csharp-dotnet/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/governance/eval.json +1881 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/pack.json +38 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/coding-style.mdc +100 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/patterns.mdc +107 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/security.mdc +86 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/evals.json +77 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/evals.json +77 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/evals.json +76 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/evals.json +77 -0
- package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
- package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/django/pack.json +43 -0
- package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
- package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
- package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
- package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
- package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
- package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
- package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
- package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
- package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
- package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/flutter-dart/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/flutter-dart/governance/eval.json +1849 -0
- package/src/gdskills/bundled/stacks/flutter-dart/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/flutter-dart/pack.json +41 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/coding-style.mdc +98 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/security.mdc +91 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/testing.mdc +101 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/evals.json +79 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/kotlin-android/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/kotlin-android/governance/eval.json +1889 -0
- package/src/gdskills/bundled/stacks/kotlin-android/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/kotlin-android/pack.json +38 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/coding-style.mdc +89 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/patterns.mdc +96 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/SKILL.md +150 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/evals.json +76 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/evals.json +78 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/SKILL.md +131 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/evals.json +77 -0
- package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
- package/src/gdskills/bundled/stacks/python/pack.json +1 -1
- package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
- package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
- package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
- package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
- package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
- package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/swift-ios/governance/eval.json +1803 -0
- package/src/gdskills/bundled/stacks/swift-ios/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/swift-ios/pack.json +38 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/coding-style.mdc +92 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/patterns.mdc +112 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/security.mdc +78 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/testing.mdc +90 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/SKILL.md +131 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/evals.json +76 -0
- package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
- package/src/gdskills/bundled/agents/python-code-auditor.md +0 -49
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Use when implementing or extending a feature in a Kotlin/Android app with Jetpack Compose -- composable state hoisting, coroutine scopes (viewModelScope/lifecycleScope), StateFlow exposure from a ViewModel, and recomposition-safe side effects.",
|
|
4
|
+
"decision": "fork",
|
|
5
|
+
"topMatch": "kotlin-android/kotlin-android-code-review",
|
|
6
|
+
"recordedAt": "2026-09-25T15:35:02.929Z",
|
|
7
|
+
"skillName": "compose-implementation",
|
|
8
|
+
"justification": "Top match kotlin-android/kotlin-android-code-review is this same pack's read-only review skill (different intent, action verb, workflow), not a real implementation duplicate; no existing implement skill covers Compose state hoisting, coroutine scopes, or StateFlow exposure."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Use when a Kotlin/Android module's test suite needs writing, extending, or fixing -- runTest/TestDispatcher for suspend functions and ViewModels, ComposeTestRule semantics-based finders for UI tests, and MockK at the repository/data-source interface boundary.",
|
|
12
|
+
"decision": "fork",
|
|
13
|
+
"topMatch": "swift-ios/swift-testing",
|
|
14
|
+
"recordedAt": "2026-09-25T15:35:03.232Z",
|
|
15
|
+
"skillName": "kotlin-android-testing",
|
|
16
|
+
"justification": "Nearest match swift-ios/swift-testing tests Swift/XCTest/Swift Testing code with a different runtime and mocking library entirely; no existing test skill covers Kotlin coroutine test dispatchers, ComposeTestRule, or MockK."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Use when reviewing a Kotlin/Android change for coroutine, Compose, and null-safety risks -- GlobalScope usage, side effects fired directly in a composable body, forced-unwrap on external data, mutable state leaked from a ViewModel, and exported manifest components. Read-only, no edits.",
|
|
20
|
+
"decision": "fork",
|
|
21
|
+
"topMatch": "kotlin-android/compose-implementation",
|
|
22
|
+
"recordedAt": "2026-09-25T15:35:03.502Z",
|
|
23
|
+
"skillName": "kotlin-android-code-review",
|
|
24
|
+
"justification": "Top match kotlin-android/compose-implementation is this same pack's implementation skill (different intent: write code vs read-only review), not a real review duplicate; no existing review skill covers Kotlin GlobalScope, Compose recomposition, or Android manifest exposure risks."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Use when a Gradle Android build, ktlintCheck, detekt, or lint task fails, or a Kotlin compile/type error blocks the build -- resolves Gradle/AGP/Kotlin version mismatches, Compose Compiler mismatches, unresolved dependencies, and lint/detekt findings with the smallest root-cause fix.",
|
|
28
|
+
"decision": "fork",
|
|
29
|
+
"topMatch": "csharp-dotnet/dotnet-build-fix",
|
|
30
|
+
"recordedAt": "2026-09-25T15:35:03.780Z",
|
|
31
|
+
"skillName": "kotlin-android-build-fix",
|
|
32
|
+
"justification": "Nearest match csharp-dotnet/dotnet-build-fix fixes dotnet/NuGet/StyleCop errors, not Gradle/AGP/Kotlin/Compose Compiler/detekt findings; each build-fix skill is scoped to its own toolchain's specific failure surface."
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "kotlin-android",
|
|
3
|
+
"family": "framework",
|
|
4
|
+
"modules": ["kotlin-android-rules", "kotlin-android-skills"],
|
|
5
|
+
"detectionMarkers": ["android"],
|
|
6
|
+
"provenance": { "origin": "authored", "sourceRef": "flow 336, Wave 4 batch 4" },
|
|
7
|
+
"stability": "experimental",
|
|
8
|
+
"skills": {
|
|
9
|
+
"implement": ["compose-implementation"],
|
|
10
|
+
"test": ["kotlin-android-testing"],
|
|
11
|
+
"review": ["kotlin-android-code-review"],
|
|
12
|
+
"build-fix": ["kotlin-android-build-fix"],
|
|
13
|
+
"migrate": []
|
|
14
|
+
},
|
|
15
|
+
"agentProfile": {
|
|
16
|
+
"displayName": "Kotlin/Android",
|
|
17
|
+
"auditFocus": [
|
|
18
|
+
"`!!` forced-unwrap on a value from an external source (network response, Intent extra, savedInstanceState) instead of a safe call (`?.`) or explicit null handling",
|
|
19
|
+
"`GlobalScope.launch`/`GlobalScope.async` inside a ViewModel or composable instead of `viewModelScope`/`lifecycleScope`, losing structured cancellation",
|
|
20
|
+
"state read or mutated directly inside a composable body instead of via `remember`/`rememberSaveable`/hoisted state, causing recomposition to re-run side effects",
|
|
21
|
+
"a suspend call, `Toast`, navigation, or analytics event fired directly in a composable body instead of inside `LaunchedEffect`/`DisposableEffect`/a callback",
|
|
22
|
+
"a ViewModel exposing `MutableStateFlow`/`MutableLiveData` directly to the UI layer instead of a read-only `StateFlow`/`LiveData` view",
|
|
23
|
+
"secrets or API keys stored in plain `SharedPreferences`, `BuildConfig` fields committed to source, or a manifest component left `exported=\"true\"` with no permission"
|
|
24
|
+
],
|
|
25
|
+
"buildCommands": [
|
|
26
|
+
"./gradlew assembleDebug",
|
|
27
|
+
"./gradlew testDebugUnitTest",
|
|
28
|
+
"./gradlew lint",
|
|
29
|
+
"./gradlew ktlintCheck || ./gradlew detekt"
|
|
30
|
+
],
|
|
31
|
+
"fixGuardrails": [
|
|
32
|
+
"never add `!!` or a blanket `@Suppress` annotation to silence a null-safety or lint warning instead of fixing the underlying nullability or logic",
|
|
33
|
+
"never widen a Gradle/AGP/Kotlin version pin or disable a lint/detekt rule repo-wide just to make one failure disappear without saying so in the report",
|
|
34
|
+
"never move a `LaunchedEffect`/`DisposableEffect` body directly into the composable function to work around a recomposition bug -- fix the key or hoist the state instead",
|
|
35
|
+
"never delete or weaken a failing test's assertion to reach a green build; fix the root cause or say the test caught a real bug"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.kt", "**/*.kts"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Kotlin/Android coding style
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic style rules to modern
|
|
11
|
+
Kotlin/Android idiom. Applies only to `*.kt`/`*.kts` files — everything
|
|
12
|
+
not Kotlin-specific still comes from the common rules this file `extends`.
|
|
13
|
+
|
|
14
|
+
## Naming and structure
|
|
15
|
+
|
|
16
|
+
- `PascalCase` for classes, objects, and composables (a `@Composable`
|
|
17
|
+
function that emits UI is named as a noun, `UserProfileCard`, not a verb);
|
|
18
|
+
`camelCase` for functions, properties, and local variables; `SCREAMING_SNAKE_CASE`
|
|
19
|
+
only for `const val` compile-time constants.
|
|
20
|
+
- A `@Composable` function that returns a value instead of emitting UI
|
|
21
|
+
(e.g. `rememberX`) still reads as a normal function name, not a noun —
|
|
22
|
+
reserve the noun-style name for functions that actually emit UI.
|
|
23
|
+
- Keep a file's top-level surface small: one primary class/object per
|
|
24
|
+
file named after it, with small private helpers colocated rather than
|
|
25
|
+
scattered into a shared `Utils.kt`.
|
|
26
|
+
- Package names are all lowercase, no underscores, matching the directory
|
|
27
|
+
layout (`com.example.feature.profile`, not `com.example.Feature.Profile`).
|
|
28
|
+
|
|
29
|
+
## Data classes vs regular classes
|
|
30
|
+
|
|
31
|
+
- Use `data class` for a type whose identity is its held values (a DTO, a
|
|
32
|
+
UI state holder, a value object needing `equals`/`hashCode`/`copy`) —
|
|
33
|
+
never a `data class` for something with real identity or mutable
|
|
34
|
+
reference semantics (an Android `Activity`, a repository, a singleton
|
|
35
|
+
manager).
|
|
36
|
+
- Prefer `copy()` with named parameters to produce a modified instance
|
|
37
|
+
over building a fresh constructor call by hand; it keeps every unrelated
|
|
38
|
+
field unchanged and the diff at a call site small when a new field is
|
|
39
|
+
added later.
|
|
40
|
+
- A `data class` holding UI state should default every field to a sane
|
|
41
|
+
initial value so `State()` (or `UiState()`) alone is a valid starting
|
|
42
|
+
point.
|
|
43
|
+
|
|
44
|
+
## Null-safety idiom
|
|
45
|
+
|
|
46
|
+
- Prefer a safe call (`?.`) or the Elvis operator (`?:`) over `!!`; a
|
|
47
|
+
forced-unwrap should be reserved for a value the type system cannot
|
|
48
|
+
express as non-null but that a prior check (or the language, e.g. inside
|
|
49
|
+
a `requireNotNull` block) has already proven non-null at that point —
|
|
50
|
+
never as a shortcut past a compiler nullability warning.
|
|
51
|
+
- Use `let` for a null-checked scope over a nullable receiver
|
|
52
|
+
(`value?.let { ... }`) when the block needs the unwrapped value once;
|
|
53
|
+
avoid chaining several `?.let` calls where a single `if (value != null)`
|
|
54
|
+
smart-cast reads more clearly.
|
|
55
|
+
- Prefer `requireNotNull(x) { "why x must be non-null here" }` over `x!!`
|
|
56
|
+
at a boundary (constructor, public function entry) — it fails with a
|
|
57
|
+
message that explains the invariant instead of a bare
|
|
58
|
+
`NullPointerException`.
|
|
59
|
+
- Model "value or absent" with a nullable type or a sealed result type,
|
|
60
|
+
not a sentinel value (`-1`, `""`) that callers have to remember to check
|
|
61
|
+
for.
|
|
62
|
+
|
|
63
|
+
## Sealed classes/interfaces for state modeling
|
|
64
|
+
|
|
65
|
+
- Model a UI or domain state with a `sealed interface`/`sealed class`
|
|
66
|
+
hierarchy (`Loading`, `Success(data)`, `Error(throwable)`) rather than a
|
|
67
|
+
single class with several nullable fields and a separate `isLoading`
|
|
68
|
+
boolean — the compiler then forces an exhaustive `when` at every call
|
|
69
|
+
site, so a new state can't be silently unhandled.
|
|
70
|
+
- Prefer a `sealed interface` over `sealed class` when a variant needs no
|
|
71
|
+
shared state or behavior; reach for `sealed class` only when the
|
|
72
|
+
variants share a common property or method implementation.
|
|
73
|
+
- Keep a sealed hierarchy's variants in the same file (or a `sealed`-file
|
|
74
|
+
group) as the parent — Kotlin does not require this, but scattering
|
|
75
|
+
variants across files makes the exhaustiveness check hard to audit by
|
|
76
|
+
reading.
|
|
77
|
+
|
|
78
|
+
## Extension functions
|
|
79
|
+
|
|
80
|
+
- Add an extension function to attach behavior to a type this module does
|
|
81
|
+
not own (or to keep a small, focused helper out of a growing class) —
|
|
82
|
+
not to route around giving a type a proper method when the type is
|
|
83
|
+
already yours to edit.
|
|
84
|
+
- Keep an extension's receiver type narrow and its name unambiguous
|
|
85
|
+
(`String.toDisplayDate()`, not a generic `Any.format()`); an extension
|
|
86
|
+
with a vague name invites collisions and surprises at the call site.
|
|
87
|
+
- Avoid extension-function sprawl on core types (`String`, `Int`, `List`)
|
|
88
|
+
for one-off, feature-specific logic — a feature-scoped extension on a
|
|
89
|
+
feature-scoped type keeps the change's blast radius visible.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.kt", "**/*.kts"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Kotlin/Android patterns
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic design guidance to idiomatic
|
|
11
|
+
Kotlin coroutines and Jetpack Compose design. Applies only to
|
|
12
|
+
`*.kt`/`*.kts` files.
|
|
13
|
+
|
|
14
|
+
## Coroutines: structured concurrency
|
|
15
|
+
|
|
16
|
+
- Launch a coroutine from a scope tied to the lifecycle that owns it —
|
|
17
|
+
`viewModelScope` inside a `ViewModel`, `lifecycleScope` inside an
|
|
18
|
+
`Activity`/`Fragment`, or a plain `coroutineScope { }`/`suspend`
|
|
19
|
+
function for work with no lifecycle owner. Never `GlobalScope.launch`/
|
|
20
|
+
`GlobalScope.async`: `GlobalScope` is unstructured — nothing cancels it
|
|
21
|
+
when the screen or the ViewModel goes away, so it outlives the UI it was
|
|
22
|
+
started for and can update state, navigate, or write to a destroyed
|
|
23
|
+
view.
|
|
24
|
+
- When a function needs to run several suspend calls concurrently, wrap
|
|
25
|
+
them in `coroutineScope { }` and use `async`/`await`, not a `launch`
|
|
26
|
+
fired without an `await` point — `coroutineScope` waits for every child
|
|
27
|
+
to finish (or cancels the rest if one fails) before returning, so a
|
|
28
|
+
caller downstream never observes a half-finished fan-out.
|
|
29
|
+
- Cancellation is cooperative: a long-running loop or CPU-bound block
|
|
30
|
+
inside a coroutine should call `ensureActive()`/check
|
|
31
|
+
`coroutineContext.isActive`, or use a cancellable suspending call
|
|
32
|
+
(`delay`, `yield`), so cancelling the parent scope actually stops it
|
|
33
|
+
instead of running to completion regardless.
|
|
34
|
+
- Switch dispatcher with `withContext(Dispatchers.IO)` around a blocking
|
|
35
|
+
call (network client without its own suspend API, disk I/O), not by
|
|
36
|
+
launching a whole new coroutine on that dispatcher — `withContext`
|
|
37
|
+
keeps the call sequential and propagates cancellation and the result
|
|
38
|
+
back to the caller.
|
|
39
|
+
|
|
40
|
+
## Compose: recomposition correctness
|
|
41
|
+
|
|
42
|
+
- Strong skipping mode is the Compose compiler default since Kotlin 2.0.20:
|
|
43
|
+
every restartable composable is skippable even with an unstable
|
|
44
|
+
parameter (a plain `List`/`MutableList`, a mutable var-holding class with
|
|
45
|
+
no `@Stable`/`@Immutable` annotation) — the compiler compares unstable
|
|
46
|
+
parameters by instance identity instead of refusing to skip. Prefer
|
|
47
|
+
stable parameters anyway (a primitive, a `data class` of stable members,
|
|
48
|
+
an immutable collection) since identity comparison still recomposes on
|
|
49
|
+
every NEW instance (e.g. a `List` rebuilt each call), where a genuinely
|
|
50
|
+
stable/immutable type can compare equal and skip.
|
|
51
|
+
- Hoist state to the nearest common ancestor that needs it (a caller, or
|
|
52
|
+
the owning `ViewModel`) instead of trapping it inside a leaf composable
|
|
53
|
+
when a sibling or the caller also needs to read or drive it; a leaf
|
|
54
|
+
composable with no external need for its state can still own it locally
|
|
55
|
+
via `remember`.
|
|
56
|
+
- Use `remember { ... }` for state that should survive recomposition but
|
|
57
|
+
not process death or configuration change (a derived value, an
|
|
58
|
+
`Animatable`, a coroutine-backed helper); use `rememberSaveable { ... }`
|
|
59
|
+
when the value must survive activity recreation (rotation, process
|
|
60
|
+
death) — typically UI-only state a `ViewModel` does not already own.
|
|
61
|
+
- Never call a suspend function, fire a one-off event (navigation,
|
|
62
|
+
`Toast`, analytics), or start a coroutine directly in a composable's
|
|
63
|
+
body — route it through `LaunchedEffect(key1, ...) { }` (re-runs when a
|
|
64
|
+
key changes) or `DisposableEffect(key) { onDispose { ... } }` (needs
|
|
65
|
+
cleanup), never as a bare statement that runs on every recomposition.
|
|
66
|
+
- Under strong skipping mode (default since Kotlin 2.0.20), the compiler
|
|
67
|
+
auto-memoizes a lambda declared inside a composable by wrapping it in an
|
|
68
|
+
implicit `remember`, keyed on its captures — a lambda passed inline to a
|
|
69
|
+
hot list item no longer needs manual hoisting for identity stability in
|
|
70
|
+
the common case. Reach for `@DontMemoize` only to opt a specific lambda
|
|
71
|
+
out (e.g. one that must run fresh every recomposition), and still hoist
|
|
72
|
+
a lambda explicitly when it closes over a value from an outer scope that
|
|
73
|
+
changes on a schedule the auto-memoization's capture-based keying
|
|
74
|
+
wouldn't track correctly.
|
|
75
|
+
|
|
76
|
+
## State exposure from a ViewModel
|
|
77
|
+
|
|
78
|
+
- Expose state from a `ViewModel` as a read-only `StateFlow`/`SharedFlow`
|
|
79
|
+
backed by a private mutable holder — `private val _state =
|
|
80
|
+
MutableStateFlow(...)` with `val state = _state.asStateFlow()` — never
|
|
81
|
+
the mutable type itself, so only the `ViewModel` can drive state
|
|
82
|
+
changes and the UI layer can only observe.
|
|
83
|
+
- Prefer `StateFlow` for "current value" UI state (a screen's latest
|
|
84
|
+
render state, always has a value) and `SharedFlow`/`Channel` for one-off
|
|
85
|
+
events (a snackbar, a navigation trigger) that should not replay a
|
|
86
|
+
stale value to a newly-subscribed collector.
|
|
87
|
+
- `LiveData` is legacy for new Compose/coroutine-first code: prefer
|
|
88
|
+
`StateFlow`/`SharedFlow` exposed from the `ViewModel` and collected with
|
|
89
|
+
`collectAsStateWithLifecycle()` in Compose. Keep `LiveData` only where a
|
|
90
|
+
project has already standardized on it or where an unconverted
|
|
91
|
+
`View`-based screen still consumes it.
|
|
92
|
+
- Collect a `ViewModel`'s flow in Compose with
|
|
93
|
+
`collectAsStateWithLifecycle()`, not a bare `collectAsState()` — the
|
|
94
|
+
lifecycle-aware variant stops collecting while the screen is stopped
|
|
95
|
+
instead of continuing to observe (and potentially update state) behind
|
|
96
|
+
a backgrounded UI.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.kt", "**/*.kts"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Kotlin/Android security
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic security rules to Android-
|
|
11
|
+
specific, OWASP Mobile-relevant risks. Applies only to `*.kt`/`*.kts`
|
|
12
|
+
files.
|
|
13
|
+
|
|
14
|
+
## Secrets and credential storage
|
|
15
|
+
|
|
16
|
+
- Never store a credential, auth token, or API secret in plain
|
|
17
|
+
`SharedPreferences` — plain `SharedPreferences` is an unencrypted XML
|
|
18
|
+
file readable on a rooted device or via backup extraction. Prefer the
|
|
19
|
+
Android Keystore directly (generate/store the key in the Keystore, use
|
|
20
|
+
it to encrypt the value yourself) over `EncryptedSharedPreferences`
|
|
21
|
+
(Jetpack `androidx.security:security-crypto`) for new code — that
|
|
22
|
+
library has known, unresolved reliability issues (a corrupted-keyset
|
|
23
|
+
failure mode with no clean recovery path) and Google's own guidance has
|
|
24
|
+
moved toward not recommending it for new applications; verify the
|
|
25
|
+
library's current status in the Android docs before reaching for it in
|
|
26
|
+
an existing codebase that already depends on it.
|
|
27
|
+
- Never hard-code an API key, client secret, or signing credential as a
|
|
28
|
+
Kotlin string literal or a `BuildConfig` field, whether the value is
|
|
29
|
+
committed or injected at build time from a local, untracked properties
|
|
30
|
+
file (`local.properties`) or a CI secret — either way, a `BuildConfig`
|
|
31
|
+
field is compiled into the APK exactly like a string literal and is
|
|
32
|
+
just as recoverable by decompiling it. Keeping the value out of source
|
|
33
|
+
control (via `local.properties`/a CI secret) only stops it leaking
|
|
34
|
+
through the repository; it does not keep it off the device. For a
|
|
35
|
+
value that must actually stay off the device, fetch it from a backend
|
|
36
|
+
at runtime instead of baking it into the build at all.
|
|
37
|
+
- Generate and store cryptographic keys through `AndroidKeyStore`
|
|
38
|
+
(`KeyGenParameterSpec`) rather than deriving or hard-coding a key in
|
|
39
|
+
application code — the Keystore keeps key material out of the app's
|
|
40
|
+
own process memory and out of extractable storage.
|
|
41
|
+
|
|
42
|
+
## Manifest and component exposure
|
|
43
|
+
|
|
44
|
+
- Every `<activity>`, `<service>`, `<receiver>`, and `<provider>` in
|
|
45
|
+
`AndroidManifest.xml` that does not need to be reachable from other
|
|
46
|
+
apps should have `android:exported="false"` (or no `exported`
|
|
47
|
+
attribute with no intent-filter, matching the modern default); a
|
|
48
|
+
component left `exported="true"` with no permission check is callable
|
|
49
|
+
by any app on the device, including one crafted to pass malformed
|
|
50
|
+
input.
|
|
51
|
+
- An exported component that must stay exported (a deep-link entry point,
|
|
52
|
+
a share target) validates every piece of the incoming `Intent` before
|
|
53
|
+
acting on it — never trusts an extra, a URI path segment, or a
|
|
54
|
+
`ContentProvider` query purely because the intent-filter matched.
|
|
55
|
+
- Guard a `ContentProvider` and any exported component with a
|
|
56
|
+
signature-level custom permission when the caller is meant to be your
|
|
57
|
+
own other app, not "any app that asks."
|
|
58
|
+
|
|
59
|
+
## Secure intent and deep-link handling
|
|
60
|
+
|
|
61
|
+
- Treat every value pulled from an incoming `Intent` (extras, deep-link
|
|
62
|
+
URI segments/query params, a `ClipData` from a share) as untrusted
|
|
63
|
+
input — validate its shape and range before using it to navigate, query
|
|
64
|
+
a database, or construct a file path; do not assume it matches what
|
|
65
|
+
your own app would have sent.
|
|
66
|
+
- Prefer an explicit `Intent` (naming the target component) for
|
|
67
|
+
inter-component communication within your own app; use an implicit
|
|
68
|
+
`Intent` only when you genuinely need another app to handle it, and
|
|
69
|
+
set the package (`setPackage`) when you only want your own app to
|
|
70
|
+
respond, to avoid intent interception by another installed app.
|
|
71
|
+
- Validate a deep-link's scheme/host/path against an allowlist before
|
|
72
|
+
using any part of it to load content or trigger an action —
|
|
73
|
+
`App Links`/verified `https` deep links reduce but do not eliminate
|
|
74
|
+
this: a query parameter or path segment can still carry attacker-
|
|
75
|
+
controlled data even through a verified link.
|
|
76
|
+
|
|
77
|
+
## Data and transport
|
|
78
|
+
|
|
79
|
+
- Use `HttpsURLConnection`/an HTTP client configured to require TLS for
|
|
80
|
+
every network call carrying credentials or personal data; do not add a
|
|
81
|
+
custom `TrustManager`/`HostnameVerifier` that accepts all certificates
|
|
82
|
+
outside of a short-lived local debug build, and never ship one that
|
|
83
|
+
way.
|
|
84
|
+
- Mark a view/field that must not appear in screenshots or the recent-
|
|
85
|
+
apps app-switcher preview (a PIN entry, payment details) with
|
|
86
|
+
`FLAG_SECURE` on the window, rather than relying on the OS default.
|
|
87
|
+
- Avoid logging PII, tokens, or full request/response bodies with
|
|
88
|
+
`Log.d`/`Log.i` in release builds — strip or gate verbose logging
|
|
89
|
+
behind a debug-only build flag so production logs (which can be pulled
|
|
90
|
+
from a device or bug report) do not leak sensitive data.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.kt", "**/*.kts"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Kotlin/Android testing
|
|
9
|
+
|
|
10
|
+
Narrows `core-common-rules`' stack-agnostic testing rules to Kotlin
|
|
11
|
+
coroutine testing, Compose UI testing, and idiomatic mocking on Android.
|
|
12
|
+
Applies only to `*.kt`/`*.kts` files.
|
|
13
|
+
|
|
14
|
+
## Coroutine test dispatchers
|
|
15
|
+
|
|
16
|
+
- Test a `suspend` function or a `ViewModel`'s coroutine-driven behavior
|
|
17
|
+
with `kotlinx-coroutines-test`'s `runTest { }`, not a real `delay` or a
|
|
18
|
+
blocking sleep — `runTest` runs on a `TestScope` whose virtual clock
|
|
19
|
+
skips real delay time, so a function that internally waits still runs
|
|
20
|
+
the test instantly.
|
|
21
|
+
- Inject a `TestDispatcher` (`StandardTestDispatcher`/
|
|
22
|
+
`UnconfinedTestDispatcher`) in place of `Dispatchers.Main`/
|
|
23
|
+
`Dispatchers.IO` for the code under test rather than letting it resolve
|
|
24
|
+
the real platform dispatcher — set `Dispatchers.setMain(testDispatcher)`
|
|
25
|
+
in a `@Before` and `Dispatchers.resetMain()` in an `@After` (or a JUnit
|
|
26
|
+
rule that wraps this) so `viewModelScope`/`Dispatchers.Main` resolves to
|
|
27
|
+
the test dispatcher instead of throwing or running on a background
|
|
28
|
+
thread the test cannot observe.
|
|
29
|
+
- Prefer `StandardTestDispatcher` (queues work, you advance it explicitly
|
|
30
|
+
with `advanceUntilIdle()`/`runCurrent()`) when a test needs to assert an
|
|
31
|
+
intermediate state (e.g. a loading state before a result arrives); use
|
|
32
|
+
`UnconfinedTestDispatcher` when the test only cares about the end state
|
|
33
|
+
and wants dispatched work to run eagerly.
|
|
34
|
+
- Never synchronize a coroutine test with `Thread.sleep`/a fixed `delay`
|
|
35
|
+
in the test body waiting for background work to finish — that is
|
|
36
|
+
exactly what `runTest`'s virtual time and `advanceUntilIdle()` replace.
|
|
37
|
+
|
|
38
|
+
## Compose UI testing
|
|
39
|
+
|
|
40
|
+
- Drive Compose UI assertions through a `ComposeTestRule`
|
|
41
|
+
(`createComposeRule()` for a standalone composable, or
|
|
42
|
+
`createAndroidComposeRule<Activity>()` when the test needs a hosting
|
|
43
|
+
Activity) — never reach into the underlying `View` tree.
|
|
44
|
+
- Find nodes with semantics-based finders
|
|
45
|
+
(`onNodeWithText`, `onNodeWithContentDescription`,
|
|
46
|
+
`onNodeWithTag(testTag)`) rather than a fragile structural/index-based
|
|
47
|
+
query; add an explicit `Modifier.testTag("...")` to a composable that
|
|
48
|
+
has no stable text/content-description to key off.
|
|
49
|
+
- Call `composeTestRule.waitUntil { ... }` (or
|
|
50
|
+
`composeTestRule.awaitIdle()`/`mainClock.advanceTimeUntil { }` under
|
|
51
|
+
manual clock control) to wait for an async UI update instead of a
|
|
52
|
+
fixed-duration sleep in the test.
|
|
53
|
+
- Assert both structure and behavior: a node's presence/text via a
|
|
54
|
+
semantics finder, and an interaction's effect (`performClick()` then
|
|
55
|
+
assert the resulting state/text), not just that a composable rendered
|
|
56
|
+
without throwing.
|
|
57
|
+
|
|
58
|
+
## Mocking at the interface boundary
|
|
59
|
+
|
|
60
|
+
- Mock a `Repository`/`ApiService`/`DataSource` interface (or another
|
|
61
|
+
clearly-owned collaborator boundary) that the class under test depends
|
|
62
|
+
on — never reach inside the class under test to stub its own private
|
|
63
|
+
methods or fields; a test that mocks internals stops testing real
|
|
64
|
+
behavior and breaks on any internal refactor.
|
|
65
|
+
- Prefer MockK for Kotlin code under test — it understands Kotlin
|
|
66
|
+
constructs Mockito's Java-first API handles awkwardly (`final` classes
|
|
67
|
+
by default, coroutines via `coEvery`/`coVerify`, extension functions,
|
|
68
|
+
object mocks via `mockkObject`) — unless a project has already
|
|
69
|
+
standardized on Mockito with `mockito-kotlin`; match what the project
|
|
70
|
+
already uses rather than introducing a second mocking library.
|
|
71
|
+
- Use `coEvery { repo.fetch() } returns result` /
|
|
72
|
+
`coVerify { repo.fetch() }` (MockK's coroutine-aware variants) for a
|
|
73
|
+
suspend function on a mock, not the plain `every`/`verify` forms, which
|
|
74
|
+
do not handle a `suspend` member correctly.
|
|
75
|
+
- Fake network/database responses at the repository or data-source
|
|
76
|
+
interface (a fake or mocked implementation returning canned data), not
|
|
77
|
+
by hitting a real network or an on-device database from a unit test —
|
|
78
|
+
reserve a real backing store for an instrumented/integration test.
|
|
79
|
+
|
|
80
|
+
## Layout and lifecycle
|
|
81
|
+
|
|
82
|
+
- Unit tests for a `ViewModel`/plain Kotlin class live under
|
|
83
|
+
`src/test/`; instrumented tests needing an Android context or a real
|
|
84
|
+
device/emulator (Compose UI tests, `Instrumentation`-backed tests) live
|
|
85
|
+
under `src/androidTest/` — match whichever the project's existing
|
|
86
|
+
layout uses, do not invent a third location.
|
|
87
|
+
- Use JUnit rules (`MainDispatcherRule` for coroutines,
|
|
88
|
+
`createComposeRule()` for Compose) to keep dispatcher/UI setup and
|
|
89
|
+
teardown out of every individual test method.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose-implementation
|
|
3
|
+
description: "Use when implementing or extending a feature in a Kotlin/Android app with Jetpack Compose -- composable state hoisting, coroutine scopes (viewModelScope/lifecycleScope), StateFlow exposure from a ViewModel, and recomposition-safe side effects."
|
|
4
|
+
triggers:
|
|
5
|
+
- "add this screen to our Android app with Compose"
|
|
6
|
+
- "hook up this ViewModel to the new composable"
|
|
7
|
+
- "wire a network call into this Android feature"
|
|
8
|
+
- "build a form screen with validation for the app"
|
|
9
|
+
- "expose loading/error state from the ViewModel to the UI"
|
|
10
|
+
- "add a button that launches a coroutine to save data"
|
|
11
|
+
- "implement this ticket in the mobile app's feature module"
|
|
12
|
+
metadata:
|
|
13
|
+
origin: authored
|
|
14
|
+
category: implement
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
17
|
+
license: "MIT"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Kotlin/Android (Jetpack Compose) implementation
|
|
21
|
+
|
|
22
|
+
Implement or extend a feature in a Kotlin/Android codebase built on
|
|
23
|
+
Jetpack Compose: composable structure, state hoisting, coroutine scope
|
|
24
|
+
choice, and state exposure from a `ViewModel`. Scoped to Kotlin/Android
|
|
25
|
+
specifically — `rules/coding-style.mdc`, `rules/patterns.mdc`, and
|
|
26
|
+
`rules/security.mdc` carry the full stack-specific rule set this skill
|
|
27
|
+
draws its checklist from; read them before writing code, not just this
|
|
28
|
+
summary.
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
|
|
32
|
+
### Step 1: Discover the project's own conventions
|
|
33
|
+
|
|
34
|
+
1. Read the module's `build.gradle(.kts)` for the Kotlin/AGP/Compose
|
|
35
|
+
Compiler versions and which architecture libraries are already
|
|
36
|
+
dependencies (`androidx.lifecycle:lifecycle-viewmodel-compose`,
|
|
37
|
+
`kotlinx-coroutines-android`, Hilt/Koin/Dagger, Compose Navigation).
|
|
38
|
+
2. Find the existing layout: is this a single-module app or a
|
|
39
|
+
feature-module setup (`:feature:profile`, `:core:ui`)? Match it; do
|
|
40
|
+
not invent a different module boundary for one change.
|
|
41
|
+
3. Read 1-2 neighboring screens/`ViewModel`s in the feature you are
|
|
42
|
+
touching for: state-holder naming (`UiState`, `ViewState`), whether
|
|
43
|
+
state is exposed as `StateFlow` or still `LiveData`, the DI pattern in
|
|
44
|
+
use, and whether a design-system component set already exists to
|
|
45
|
+
reuse instead of building raw `Text`/`Button` composables.
|
|
46
|
+
|
|
47
|
+
### Step 2: Design before writing
|
|
48
|
+
|
|
49
|
+
- Decide, per new piece of state: does it belong in the `ViewModel`
|
|
50
|
+
(survives configuration change, drives business logic) or hoisted only
|
|
51
|
+
as far as the nearest composable ancestor that needs it (purely
|
|
52
|
+
presentational, e.g. whether a dropdown is expanded)? Do not default
|
|
53
|
+
everything into the `ViewModel` when a leaf composable's own `remember`
|
|
54
|
+
is enough, and do not trap logic-relevant state in a composable when a
|
|
55
|
+
sibling or rotation needs it to survive.
|
|
56
|
+
- Model the screen's state as a `sealed interface`/`data class` (a single
|
|
57
|
+
`UiState` with a `sealed` status field, or a small sealed hierarchy for
|
|
58
|
+
distinct screens like `Loading`/`Content`/`Error`) rather than several
|
|
59
|
+
independent nullable/boolean fields the UI has to reconcile by hand.
|
|
60
|
+
- Decide the coroutine scope for any new asynchronous work before writing
|
|
61
|
+
it: `viewModelScope` for anything driven from a `ViewModel`,
|
|
62
|
+
`lifecycleScope`/`rememberCoroutineScope()` only for UI-only work with
|
|
63
|
+
no `ViewModel` involved (an animation trigger, a one-off scroll). Never
|
|
64
|
+
`GlobalScope`.
|
|
65
|
+
- Trace where any new side effect (navigation, a snackbar/analytics
|
|
66
|
+
event, a suspend call) needs to run from — inside a composable body it
|
|
67
|
+
belongs in `LaunchedEffect`/`DisposableEffect`, never as a bare
|
|
68
|
+
statement.
|
|
69
|
+
|
|
70
|
+
### Step 3: Implement
|
|
71
|
+
|
|
72
|
+
1. Expose new `ViewModel` state as `private val _state =
|
|
73
|
+
MutableStateFlow(...)` / `val state = _state.asStateFlow()`; collect
|
|
74
|
+
it in Compose with `collectAsStateWithLifecycle()`.
|
|
75
|
+
2. Keep composable parameters stable (primitives, `data class`es of
|
|
76
|
+
stable members, immutable collections) — strong skipping mode (default
|
|
77
|
+
since Kotlin 2.0.20) lets the compiler skip even an unstable parameter
|
|
78
|
+
via instance-identity comparison, but only a genuinely stable/immutable
|
|
79
|
+
type can compare EQUAL across calls and actually skip when nothing
|
|
80
|
+
changed; an unstable type rebuilt fresh each call still recomposes.
|
|
81
|
+
3. Hoist state per `rules/patterns.mdc`; use `remember`/`rememberSaveable`
|
|
82
|
+
for composable-local state, never a `var` mutated directly inside the
|
|
83
|
+
composable body outside of a `remember` holder.
|
|
84
|
+
4. Route any new side effect through `LaunchedEffect(key1, ...) { }` (with
|
|
85
|
+
the correct key so it re-runs exactly when it should) or
|
|
86
|
+
`DisposableEffect(key) { onDispose { ... } }` when cleanup is needed.
|
|
87
|
+
5. Launch new coroutine work in `viewModelScope`/`lifecycleScope` as
|
|
88
|
+
decided in Step 2; wrap concurrent suspend calls in `coroutineScope { }`
|
|
89
|
+
with `async`/`await` rather than firing untracked `launch` calls.
|
|
90
|
+
6. Avoid `!!`; use `?.`/`?:`/`requireNotNull(x) { "..." }` for any value
|
|
91
|
+
that can be null (a nullable response field, an `Intent` extra, a
|
|
92
|
+
`savedStateHandle` lookup).
|
|
93
|
+
|
|
94
|
+
### Step 4: Verify
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
./gradlew assembleDebug
|
|
98
|
+
./gradlew testDebugUnitTest
|
|
99
|
+
./gradlew lint
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run `./gradlew ktlintCheck` or `./gradlew detekt` if the project has one
|
|
103
|
+
configured. Fix findings at the root cause per `rules/security.mdc` and
|
|
104
|
+
`rules/coding-style.mdc`; a build/test/lint failure here is a signal to
|
|
105
|
+
fix the implementation, not to reach for `kotlin-android-build-fix`'s
|
|
106
|
+
scope unless the failure is purely a build/dependency/toolchain problem
|
|
107
|
+
unrelated to the feature logic.
|
|
108
|
+
|
|
109
|
+
### Step 5: Report
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Implemented: feature/profile/ProfileViewModel.kt, feature/profile/ProfileScreen.kt
|
|
113
|
+
- New ProfileUiState sealed hierarchy (Loading/Content/Error)
|
|
114
|
+
- State exposed as StateFlow, collected with collectAsStateWithLifecycle()
|
|
115
|
+
- Save action launched in viewModelScope
|
|
116
|
+
- ./gradlew assembleDebug/testDebugUnitTest/lint all pass
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Rules
|
|
120
|
+
|
|
121
|
+
- Never launch a coroutine with `GlobalScope` — use `viewModelScope`,
|
|
122
|
+
`lifecycleScope`, or a scope passed in by the caller.
|
|
123
|
+
- Never call a suspend function or fire a one-off UI event directly in a
|
|
124
|
+
composable's body — use `LaunchedEffect`/`DisposableEffect`.
|
|
125
|
+
- Never expose a mutable `MutableStateFlow`/`MutableLiveData` from a
|
|
126
|
+
`ViewModel`'s public surface — expose the read-only view.
|
|
127
|
+
- Never use `!!` to route around a null-safety warning; use a safe call,
|
|
128
|
+
Elvis operator, or `requireNotNull` with a message.
|
|
129
|
+
|
|
130
|
+
## Red Flags
|
|
131
|
+
|
|
132
|
+
| Rationalization | Why it is wrong |
|
|
133
|
+
|---|---|
|
|
134
|
+
| "I'll just launch this in GlobalScope, it's a quick one-off network call" | Nothing cancels a GlobalScope coroutine when the screen/ViewModel is destroyed; it can still update state or navigate on a gone screen |
|
|
135
|
+
| "I'll call this suspend function right in the composable body, it only runs once" | A composable body can re-run on every recomposition; an uncontrolled suspend call outside LaunchedEffect can fire far more than once, or not track the right restart key |
|
|
136
|
+
| "I'll add `!!` here since this value is always set by the time we get here" | "Always" is an assumption the compiler cannot verify; a safe call or `requireNotNull` with a message documents and enforces the same assumption instead of crashing silently when it's wrong |
|
|
137
|
+
| "I'll just expose the MutableStateFlow directly, it saves a line" | Lets any collector outside the ViewModel push a new value, breaking the single-writer invariant the pattern exists to guarantee |
|
|
138
|
+
|
|
139
|
+
## Verification
|
|
140
|
+
|
|
141
|
+
Do not report the work done until all of the following hold:
|
|
142
|
+
|
|
143
|
+
- `./gradlew assembleDebug`, `./gradlew testDebugUnitTest`, and
|
|
144
|
+
`./gradlew lint` all exit 0.
|
|
145
|
+
- Every new coroutine launch uses `viewModelScope`/`lifecycleScope`/a
|
|
146
|
+
passed-in scope, never `GlobalScope`.
|
|
147
|
+
- Every new side effect inside a composable body runs through
|
|
148
|
+
`LaunchedEffect`/`DisposableEffect`, not as a bare statement.
|
|
149
|
+
- Every new/touched `ViewModel`-exposed state is read-only from the UI's
|
|
150
|
+
perspective (`StateFlow`/`SharedFlow`, not the mutable type).
|