skills/kmp-feature-slice/SKILL.md
Procedural KMP feature generation workflow — step-by-step creation of feature slices with typed errors, compose-arch compliance, and build verification. Use when scaffolding a new feature module in a KMP/Compose Multiplatform project.
npx skillsauth add andvl1/claude-plugin kmp-feature-sliceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Procedural workflow for generating complete KMP feature slices. Enforces strict creation order, typed error handling, and compose-arch compliance.
Prerequisites: Read compose-arch skill first — it is the SINGLE SOURCE OF TRUTH for architecture rules.
compose-arch.@BindingContainer, @Provides, @AssistedInject) — metro-di-mobile (Metro 1.0.0).Value<T>, ChildStack) — decompose.ktor-client (3.4.3).When generating a slice, always read these companion skills first for current versions and idioms; never inline a version here.
This skill references three helpers that are project conventions, not Decompose/Metro/Kotlin APIs. Define them once in your :core:* modules (or substitute equivalents) before following the steps. If your project already has equivalents under different names, mentally remap.
| Helper | Lives in | Minimal definition / stand-in |
|--------|----------|-------------------------------|
| componentScope() | :core:component (typical) | Extension on ComponentContext returning a CoroutineScope tied to component lifecycle. Stand-in: Decompose Essenty's coroutineScope() from essenty-coroutines-extensions — same semantics. |
| runCatchingApp { ... } | :core:result (typical) | Project-level runCatching variant that maps Throwable to AppResult.Failure (typed-error mapping inside). Stand-in: plain kotlin.runCatching { ... } returning kotlin.Result<T>. |
| AppResult<T> | :core:result (typical) | Project sealed result type — usually sealed interface AppResult<out T> { data class Success<T>(val value: T); data class Failure(val error: AppError) }. Stand-in: kotlin.Result<T>. |
If these don't exist yet in the project, create them once in :core:component / :core:result (or whatever module convention the host project uses) before generating feature slices. Do NOT inline them per-feature.
Below, treat any reference to componentScope() / runCatchingApp / AppResult as a call into these conventions — substitute the stand-ins listed above if the project hasn't adopted them.
Collect these inputs before generating any code:
| Input | Required | Example |
|-------|----------|---------|
| Feature name | Yes | OrderHistory |
| Data sources | Yes | remote-only, local+remote, local-only |
| Target platforms | Yes | android,ios,desktop, android,ios,desktop,wasm, all |
| Error types | Yes | network,validation, network,auth,conflict |
| Has list/detail? | Yes | list-only, detail-only, list+detail |
| Navigation | Yes | stack (full screen), slot (dialog/modal), none |
| Needs pagination? | No | true/false (default: false) |
| Parent module path | Yes | feature/order-history |
Select from these typed error categories (see references/error-patterns.md for sealed class templates):
| Error Type | When to Use |
|------------|-------------|
| network | API calls, timeouts, connectivity |
| validation | User input, form data |
| auth | Token expired, unauthorized |
| conflict | Concurrent modification, duplicate |
| not-found | Missing resource |
| storage | Database, file system errors |
| permission | OS-level permissions (camera, location) |
Based on inputs, generate the exact file list. Every feature slice follows this structure:
feature/<name>/
├── api/
│ └── src/commonMain/kotlin/
│ ├── <Name>Component.kt # Step 1: Interface
│ ├── <Name>Models.kt # Step 2: Domain models + error types
│ └── <Name>Repository.kt # Step 3: Repository interface (if data sources != none)
│
└── impl/
└── src/commonMain/kotlin/
├── component/
│ └── Default<Name>Component.kt # Step 7: Component implementation
├── domain/
│ ├── usecase/
│ │ └── Get<Name>UseCase.kt # Step 5: Primary use case
│ └── repository/
│ └── <Name>RepositoryImpl.kt # Step 6: Repository implementation
├── data/
│ └── datasource/
│ └── <Name>RemoteDataSource.kt # Step 4: Remote data source
├── view/
│ ├── <Name>ViewState.kt # Step 8: View state
│ ├── <Name>ViewEvent.kt # Step 9: View events
│ └── <Name>View.kt # Step 10: View (pure UI)
├── screen/
│ └── <Name>Screen.kt # Step 11: Screen (thin adapter)
└── di/
└── <Name>Module.kt # Step 12: DI module
| Condition | Additional Files | Insert at step |
|-----------|-----------------|---------------|
| data-sources: local+remote | <Name>LocalDataSource.kt | Step 4b — same step as RemoteDataSource |
| local storage uses DataStore (e.g. Settings-style features) | commonMain declares expect factory + interface; platform sourceSets supply actuals — typically androidMain/.../<Name>Settings.android.kt and iosMain/.../<Name>Settings.ios.kt (and desktopMain / wasmJsMain if targeted). Do NOT put DataStore construction in commonMain. | Step 4b — same step as LocalDataSource |
| has: list+detail | Container Component (<Name>Component.kt) owning ChildStack<Config, Child> + two child Components: <Name>ListComponent.kt and <Name>DetailComponent.kt (each with its own viewState/Event/View/Screen). Impl side mirrors with Default<Name>Component, Default<Name>ListComponent, Default<Name>DetailComponent. | Container interface at Step 1; child interfaces also at Step 1; container impl at Step 7 (drives ChildStack, exposes child factories); child impls at Step 7 (each owns its own state/use cases). View/State/Event/Screen for each child at Steps 8-11. |
| pagination: true | <Name>Pager.kt in impl/domain/ | Step 5b — between Repository (Step 5) and Component (Step 7) |
| navigation: stack | Navigation config (Config sealed class + ChildStack) in <Name>Component.kt and Default<Name>Component.kt | Steps 1 + 7 |
| navigation: slot | Slot config in <Name>Component.kt and Default<Name>Component.kt | Steps 1 + 7 |
Component owning navigation alongside state. A Component can expose BOTH
val viewState: Value<T>ANDval childStack: Value<ChildStack<Config, Child>>. They are independent fields.List+detail uses the container pattern, not one Component with two states. The container Component holds only the
ChildStackand child factories — noviewStateof its own. Each child Component (List, Detail) owns its ownviewState, events, and use cases. This keeps each child testable in isolation and matches Decompose'sChildren { }rendering. A real example:// Container — navigation only, no viewState interface OrderHistoryComponent { val childStack: Value<ChildStack<*, Child>> sealed class Child { data class List(val component: OrderHistoryListComponent) : Child() data class Detail(val component: OrderHistoryDetailComponent) : Child() } } // Each child is a full Component with its own viewState interface OrderHistoryListComponent { val viewState: Value<OrderHistoryListViewState> fun obtainEvent(event: OrderHistoryListEvent) }Avoid the anti-pattern of a single Component carrying
viewState: Value<ListState>+detailViewState: Value<DetailState>+ manual show/hide flags — that scales poorly and breaks the one-Component-one-screen mental model.
feature/<name>/
├── api/
│ └── build.gradle.kts # Step 13: API module build
└── impl/
└── build.gradle.kts # Step 14: Impl module build
Update settings.gradle.kts — Step 15.
Generate files in this exact order. Each step depends on previous steps.
interface <Name>Component {
val viewState: Value<<Name>ViewState>
fun obtainEvent(event: <Name>Event)
fun interface Factory {
fun create(
componentContext: ComponentContext,
onNavigate: (<NavigationType>) -> Unit // from navigation input
): <Name>Component
}
}
references/error-patterns.md)AppResult<T> for all operationsexpect fun create<Name>DataStore(): DataStore<Preferences> in commonMain and provide actual impls in androidMain (<Name>Settings.android.kt), iosMain (<Name>Settings.ios.kt), and any other targeted platform. The interface stays in commonMain; only the factory is expect/actual.execute() function returning Result<T>error-patterns.md for typed error creation@Inject + @Assisted patternValue<T> for state (NOT StateFlow)componentScope() for coroutinesobtainEvent()references/compose-ui-templates.md for templateslist+detail and an internal Children { when } block, ~25–30 lines is acceptable — the rule is "no logic", not a hard line count.@BindingContainer with @Provides bindings — one module per feature.api/ whose impl lives in impl/, prefer @DefaultBinding(<ApiInterface>::class) on the impl class over a hand-written @Provides — Metro auto-binds and you avoid one boilerplate function per interface.@Provides in the module only when:
HttpClient into a feature-scoped service), ORFactory to its impl-side counterpart (e.g. mapping OrderHistoryComponent.Factory → DefaultOrderHistoryComponent.Factory).object vs class @BindingContainer: both work identically in Metro 1.0. Default to object (no instance state, matches common KMP convention) unless the host project uses class. Canonical example:@BindingContainer
object OrderHistoryModule {
// Use cases + repository auto-bind via @Inject / @DefaultBinding — nothing needed here.
// @Provides only when wiring api Factory ← impl Factory or composing several deps.
@Provides
fun provideComponentFactory(
impl: DefaultOrderHistoryComponent.Factory,
): OrderHistoryComponent.Factory = impl
}
@Inject.Metro 1.0 supports the full KMP target matrix — including js(IR), wasmJs, wasmWasi, and all native targets (see metro-di-mobile skill's "Supported KMP targets" table). So an api/ module that targets android+ios+desktop+js+wasmJs can still apply the Metro plugin and use @Inject on its use cases. Web frontends consuming the api module will compile fine.
The only reason to keep an api/ module Metro-free is when the consumer doesn't run Metro at all — for example, a non-KMP Kotlin/JS app that pulls in only the api artifact and wires use cases by hand. In that case:
api/ stay as plain Kotlin classes (no annotations).impl/ module provides them via @Provides in <Name>Module.kt for the mobile path.GetOrderHistoryUseCase(JsOrderHistoryRepository())) at the call site.Default to "Metro everywhere" unless you have a concrete reason to opt out.
Once the api module carries @Inject annotations, each consumer (mobile vs web) defines its own @DependencyGraph and reuses the same api use cases. The two graphs differ only in which impl modules they pull in.
Mobile graph — pulls the impl module (with Decompose, real DataSources, full Compose UI):
// composeApp/.../di/AndroidAppGraph.kt
@DependencyGraph(
bindingContainers = [
NetworkModule::class,
AndroidPlatformModule::class,
OrderHistoryModule::class, // impl-side @BindingContainer
]
)
interface AndroidAppGraph {
val orderHistoryComponentFactory: OrderHistoryComponent.Factory
}
Web graph — only the api module + a web-specific repo impl. No Decompose, no impl module:
// webApp/.../di/WebAppGraph.kt
@DependencyGraph(
bindingContainers = [WebPlatformModule::class],
)
interface WebAppGraph {
val getOrderHistoryPageUseCase: GetOrderHistoryPageUseCase
val getOrderDetailUseCase: GetOrderDetailUseCase
val orderHistoryRepository: OrderHistoryRepository
}
// webApp/.../orderhistory/JsOrderHistoryRepository.kt
@Inject
@DefaultBinding(OrderHistoryRepository::class)
class JsOrderHistoryRepository(
private val httpClient: HttpClient,
) : OrderHistoryRepository {
override suspend fun getPage(cursor: String?): AppResult<OrderPage> = /* ... */
override suspend fun getDetail(id: String): AppResult<OrderDetail> = /* ... */
}
The use cases (GetOrderHistoryPageUseCase, GetOrderDetailUseCase) live in api with @Inject and resolve from constructor scanning — no @Provides needed in the web module. The @DefaultBinding on JsOrderHistoryRepository plugs the api interface. The web graph deliberately excludes OrderHistoryModule (impl side) since it pulls Decompose lifecycle types React doesn't need.
For createGraph lifetime on JS (top-level lazy vs React Context provider), see metro-di-mobile's "JS / WASM specifics" section.
settings.gradle.ktscomposeApp/build.gradle.ktsAfter generation, verify ALL of these:
Value<T>, not StateFlowResult<T>AppResult<T>./gradlew :<module>:assemble succeeds# Verify API module compiles
./gradlew :feature:<name>:api:assemble
# Verify impl module compiles
./gradlew :feature:<name>:impl:assemble
# Verify full app compiles with new feature
./gradlew :composeApp:assemble
Load these on demand — do NOT read all upfront:
| Reference | When to Load |
|-----------|-------------|
| references/error-patterns.md | Step 2 (domain models + errors) |
| references/compose-ui-templates.md | Steps 8-11 (UI layer) |
| references/kotlin-web-templates.md | When target includes wasm or js |
| Skill | Relationship |
|-------|-------------|
| compose-arch | SOURCE OF TRUTH for architecture rules. This skill adds generation order on top. |
| decompose | Navigation patterns. Load for Step 7 (component) if complex navigation. |
| metro-di-mobile | DI patterns. Load for Step 12 (module). |
| kmp | Project structure. Load for Steps 13-15 (gradle). |
| kotlin-web | Web-specific patterns. Load when targets include wasm/js. |
development
Effective Go patterns — idiomatic code, testing, benchmarks, project layout. Always use Go 1.21+ patterns.
development
Go microservices — gRPC, REST, cloud-native patterns, service discovery, circuit breakers, observability, health checks, graceful shutdown.
development
Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance.
testing
Android WorkManager for guaranteed background execution - use for deferred tasks, periodic syncs, file uploads, notifications, and task chains. Covers CoroutineWorker, constraints, chaining, testing, and troubleshooting. Use when implementing background work that needs reliable execution across app restarts and doze mode.