mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
232
.codex/skills/compose-skill/references/cross-platform.md
Normal file
232
.codex/skills/compose-skill/references/cross-platform.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Cross-Platform (KMP) Specifics
|
||||
|
||||
## Sharing Strategy
|
||||
|
||||
Share these first: reducers, ViewModels, validators, calculators, formatting policies, screen state models, most screen UI.
|
||||
|
||||
Keep platform-specific until proven otherwise: permissions, share sheets, clipboard, haptics, file pickers, notifications, deep links, review prompts, platform input traits, OS navigation shell.
|
||||
|
||||
## Placement Guide
|
||||
|
||||
### What belongs in `commonMain`
|
||||
|
||||
Feature state, intents/messages, reducer/ViewModel logic, calculators, validators, eligibility, repository interfaces, use cases that earn their keep, shared composables, presentation mapping, semantic nav effects and error keys.
|
||||
|
||||
### What should remain platform-specific
|
||||
|
||||
Runtime permissions, share/open sheet, haptics, clipboard, URLs, billing, notifications, biometrics, manifest/delegate deep links, OS widgets/shortcuts.
|
||||
|
||||
### Placement Table
|
||||
|
||||
| Concern | Default placement | Why |
|
||||
|---|---|---|
|
||||
| reducer/ViewModel | `commonMain` | pure, testable, reusable |
|
||||
| validator/calculator | `commonMain` | pure domain logic |
|
||||
| repository contract | `commonMain` | shared dependency boundary |
|
||||
| haptics/share/clipboard | interface + platform impl | app capability, easy to fake |
|
||||
| locale/number/date formatter | interface or shared library | locale-sensitive behavior |
|
||||
| resource identifiers | `commonMain` UI | shared UI uses shared resources |
|
||||
| permission prompt flow | platform-specific | OS-specific behavior |
|
||||
| safe-area / keyboard handling | route/UI boundary | platform behavior differs |
|
||||
| navigation controller binding | platform/UI shell | ViewModel should not know controller type |
|
||||
| analytics SDK integration | platform or shared facade | real implementation differs |
|
||||
|
||||
### Dependency Verification for commonMain
|
||||
|
||||
**Before claiming `commonMain`:** confirm multiplatform artifacts exist. Much of AndroidX is still Android-only; some libs publish KMP (e.g. `lifecycle-viewmodel`, `datastore-preferences`) with version-dependent surfaces. Check Maven for `-jvm`, `-iosarm64`, `-iosX64`, etc.; use context7 `resolve-library-id` + `query-docs` when available. **If unverifiable**, say so—use platform placement or wrapper interfaces.
|
||||
|
||||
## Interfaces vs expect/actual
|
||||
|
||||
### Default recommendation
|
||||
|
||||
Use **interfaces** for app capabilities: haptics, clipboard, share, URL opener, analytics, date/number formatting, file opener.
|
||||
|
||||
Use **`expect/actual`** for thin platform facts or one-off helpers when an interface buys little.
|
||||
|
||||
### Practical rule
|
||||
|
||||
- **Interface** when the capability has lifetime, DI, fakes, or multiple implementations
|
||||
- **`expect/actual`** when it is a tiny platform hook with no domain meaning
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Heavy/async/hardware services (GPS, biometrics, keystore): `commonMain` interface + Koin (or similar) for platform impls. Reserve `expect/actual` for tiny sync primitives (UUID, dates, clipboard).
|
||||
|
||||
## Platform Bridge Patterns
|
||||
|
||||
The rules above cover *when* to prefer interfaces vs `expect/actual`; below is *how* to wire each pattern.
|
||||
|
||||
### Choosing the Right Bridge
|
||||
|
||||
| Need | Pattern | Why |
|
||||
|---|---|---|
|
||||
| Service with lifecycle, state, or async (player, auth, payments, analytics) | Interface + DI | Testable, fakeable, swappable impls |
|
||||
| Stateless platform fact (UUID, platform name, default locale) | `expect/actual` function | No DI overhead for a one-liner |
|
||||
| Reuse existing platform type in common signature | `expect class` + `actual typealias` | Rare — prefer interface when possible |
|
||||
|
||||
### Pattern 1: Interface + DI (Primary)
|
||||
|
||||
Contract in `commonMain`; platform modules supply impls; DI binds them. ViewModel depends only on the interface. Koin setup: [koin.md](koin.md).
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
interface Player { fun play(uri: String); fun pause(); fun release() }
|
||||
|
||||
// androidMain
|
||||
class AndroidPlayer(private val context: Context) : Player {
|
||||
private val mp = MediaPlayer()
|
||||
override fun play(uri: String) { mp.setDataSource(context, uri.toUri()); mp.start() }
|
||||
override fun pause() = mp.pause()
|
||||
override fun release() = mp.release()
|
||||
}
|
||||
|
||||
// iosMain
|
||||
class IosPlayer : Player {
|
||||
private var av: AVPlayer? = null
|
||||
override fun play(uri: String) { av = AVPlayer(uRL = NSURL(string = uri)); av?.play() }
|
||||
override fun pause() { av?.pause() }
|
||||
override fun release() { av = null }
|
||||
}
|
||||
|
||||
// androidMain
|
||||
val androidPlayerModule = module { single<Player> { AndroidPlayer(get()) } }
|
||||
// iosMain
|
||||
val iosPlayerModule = module { single<Player> { IosPlayer() } }
|
||||
|
||||
class PlayerViewModel(private val player: Player) : ViewModel() {
|
||||
fun onEvent(e: PlayerEvent) {
|
||||
when (e) { is PlayerEvent.Play -> player.play(e.uri); PlayerEvent.Pause -> player.pause() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: expect/actual for Thin Primitives
|
||||
|
||||
Stateless one-liners, no DI/interface/fakes:
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect fun randomUUID(): String
|
||||
|
||||
// androidMain
|
||||
actual fun randomUUID(): String = java.util.UUID.randomUUID().toString()
|
||||
|
||||
// iosMain
|
||||
actual fun randomUUID(): String = platform.Foundation.NSUUID().UUIDString()
|
||||
```
|
||||
|
||||
### Pattern 3: expect/actual with Typealias
|
||||
|
||||
When a platform type already matches the contract:
|
||||
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect class PlatformDate {
|
||||
fun toEpochMillis(): Long
|
||||
}
|
||||
|
||||
// jvmMain
|
||||
actual typealias PlatformDate = java.time.Instant
|
||||
|
||||
// nativeMain
|
||||
actual class PlatformDate(private val nsDate: NSDate) {
|
||||
actual fun toEpochMillis(): Long = (nsDate.timeIntervalSince1970 * 1000).toLong()
|
||||
}
|
||||
```
|
||||
|
||||
Prefer interface+DI for fakes or when types do not match 1:1.
|
||||
|
||||
### Bridge Anti-Patterns
|
||||
|
||||
- `expect/actual` for lifecycle/state/async → interface+DI
|
||||
- Platform imports in `commonMain` (compiler flags; still catch in review)
|
||||
- Fat `expect/actual` → thin bridge, logic in impls
|
||||
- Skipping interfaces when tests need fakes
|
||||
|
||||
## Lifecycle
|
||||
|
||||
`lifecycle-viewmodel` / `lifecycle-runtime-compose` can expose `ViewModel`, `viewModelScope`, `collectAsStateWithLifecycle` in `commonMain`; not all Lifecycle APIs are MP—depends on androidx/KMP.
|
||||
|
||||
- Artifact must publish KMP targets (`-jvm`, `-iosarm64`, …) and expose the API on MP (many APIs stay Android-only); match project targets.
|
||||
- **Confirm versions** via context7 or AndroidX notes; if not, say so—wrap platform lifecycle behind interfaces if needed.
|
||||
- **Typical in `commonMain` (re-verify):** `ViewModel`, `viewModelScope`, `collectAsStateWithLifecycle`, `koinViewModel()`.
|
||||
|
||||
## State Restoration
|
||||
|
||||
- `rememberSaveable`: small local UI state only
|
||||
- Cross-platform drafts: rehydrate from persistence, not assumed OS restoration parity
|
||||
- Serialize ViewModel state only when product requires it
|
||||
|
||||
## Keyboard, Focus, and Input
|
||||
|
||||
- Test text input on real iOS hardware; isolate quirks at the UI/platform edge
|
||||
- No keyboard workaround flags in reducer state; shared UI uses inset/safe-area layout
|
||||
- Keep selection/composition local per field when needed
|
||||
|
||||
## Safe Area and Layout
|
||||
|
||||
Insets-aware shared layouts; verify safe areas, keyboard overlap, sheets, nav chrome. Never put “iOS safe-area hack” into feature state.
|
||||
|
||||
## Platform Capabilities
|
||||
|
||||
Model haptics, clipboard, share as semantic effects; shell executes them.
|
||||
|
||||
```kotlin
|
||||
enum class HapticType { Confirm, Error, Selection }
|
||||
interface Haptics { fun perform(type: HapticType) }
|
||||
sealed interface ProductEffect {
|
||||
data class TriggerHaptic(val type: HapticType) : ProductEffect
|
||||
data class ShareQuote(val text: String) : ProductEffect
|
||||
}
|
||||
interface ShareText { suspend fun share(text: String) }
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
CMP shared resources (strings, images, fonts, qualifiers, localization, Gradle setup). Full API surface: **[Multiplatform Resources](resources.md)**.
|
||||
|
||||
```kotlin
|
||||
enum class ValidationMessageKey { Required, InvalidNumber, MustBePositive }
|
||||
|
||||
@Composable
|
||||
fun ValidationMessage(messageKey: ValidationMessageKey?) {
|
||||
val text = when (messageKey) {
|
||||
ValidationMessageKey.Required -> stringResource(Res.string.error_required)
|
||||
ValidationMessageKey.InvalidNumber -> stringResource(Res.string.error_invalid_number)
|
||||
ValidationMessageKey.MustBePositive -> stringResource(Res.string.error_must_be_positive)
|
||||
null -> return
|
||||
}
|
||||
Text(text = text, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### GOOD: shared calculator
|
||||
|
||||
```kotlin
|
||||
class PriceCalculator {
|
||||
fun calculate(d: PriceDraft): PriceDerived {
|
||||
val w = if (d.includeWaste) 1.10 else 1.0
|
||||
val mat = d.area * d.materialRate * w
|
||||
val lab = d.area * d.laborRate
|
||||
val sub = mat + lab
|
||||
val tax = sub * (d.taxPercent / 100.0)
|
||||
return PriceDerived(mat, lab, sub, tax, sub + tax)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BAD: platform leakage in state
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class ProductState(
|
||||
val input: ProductInput = ProductInput(),
|
||||
val iosKeyboardInsetHack: Int = 0,
|
||||
val androidHapticPattern: String = "",
|
||||
val shareSheetPresented: Boolean = false,
|
||||
)
|
||||
```
|
||||
|
||||
Platform leakage.
|
||||
Reference in New Issue
Block a user