| name | kmp-feature-slice |
| description | 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. |
KMP Feature Slice Generator
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.
Companion skills (versions live there — do not duplicate)
- Architecture rules + Component/View/Screen split —
compose-arch.
- DI annotations (
@BindingContainer, @Provides, @AssistedInject) — metro-di-mobile (Metro 1.0.0).
- Navigation primitives (
Value<T>, ChildStack) — decompose.
- HTTP —
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.
Project conventions (project-level glue, NOT framework)
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.
Phase 1: Input Collection (LOW FREEDOM)
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 |
Error Type Catalog
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) |
Phase 2: File Manifest (ZERO FREEDOM)
Based on inputs, generate the exact file list. Every feature slice follows this structure:
Mandatory Files (always created)
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
Conditional Files
| 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> AND val 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 ChildStack and child factories — no viewState of its own. Each child Component (List, Detail) owns its own viewState, events, and use cases. This keeps each child testable in isolation and matches Decompose's Children { } rendering. A real example:
interface OrderHistoryComponent {
val childStack: Value<ChildStack<*, Child>>
sealed class Child {
data class List(val component: OrderHistoryListComponent) : Child()
data class Detail(val component: OrderHistoryDetailComponent) : Child()
}
}
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.
Gradle Files
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.
Phase 3: Generation Order (STRICT SEQUENCE)
Generate files in this exact order. Each step depends on previous steps.
Step 1: Component Interface (api/)
interface <Name>Component {
val viewState: Value<<Name>ViewState>
fun obtainEvent(event: <Name>Event)
fun interface Factory {
fun create(
componentContext: ComponentContext,
onNavigate: (<NavigationType>) -> Unit
): <Name>Component
}
}
Step 2: Domain Models + Typed Errors (api/)
- Domain data classes
- Sealed error hierarchy (see
references/error-patterns.md)
- Keep models serializable if used in navigation configs
Step 3: Repository Interface (api/)
- Return
AppResult<T> for all operations
- Define typed error mapping
Step 4: Data Sources (impl/data/)
- Remote data source with Ktor client
- (Optional) Local data source with Room/DataStore
- DataStore needs platform-specific construction → declare
expect 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.
- Map API DTOs to domain models here
Step 5: Use Cases (impl/domain/usecase/)
- Single
execute() function returning Result<T>
- All error handling and mapping here
- Reference
error-patterns.md for typed error creation
Step 6: Repository Implementation (impl/domain/repository/)
- Coordinate data sources
- Cache strategy (if local+remote)
Step 7: Component Implementation (impl/component/)
@Inject + @Assisted pattern
Value<T> for state (NOT StateFlow)
componentScope() for coroutines
- Event handling via
obtainEvent()
- Navigation via Decompose callbacks
Step 8-9: ViewState + ViewEvent (impl/view/)
- Sealed class for ViewState: Loading, Success, Error (+ Empty if list)
- Sealed class for ViewEvent: all user interactions
Step 10: View (impl/view/)
- Pure UI, only viewState + eventHandler params
- NO remember, NO side effects
- See
references/compose-ui-templates.md for templates
Step 11: Screen (impl/screen/)
- Thin adapter: subscribe to component state, pass to View
- Target ~20 lines for a single-view screen. With
list+detail and an internal Children { when } block, ~25–30 lines is acceptable — the rule is "no logic", not a hard line count.
Step 12: DI Module (impl/di/)
@BindingContainer with @Provides bindings — one module per feature.
- For each interface declared in
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.
- Use
@Provides in the module only when:
- the binding requires plumbing several deps together (e.g. wrapping a
HttpClient into a feature-scoped service), OR
- the binding wires an api-side
Factory 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 {
@Provides
fun provideComponentFactory(
impl: DefaultOrderHistoryComponent.Factory,
): OrderHistoryComponent.Factory = impl
}
Step 13-14: Gradle Build Files
- API module: kotlin multiplatform only, no compose. Apply the Metro plugin if you want use cases to carry
@Inject.
- Impl module: kotlin multiplatform + compose + decompose + Metro plugin.
Cross-target api note
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:
- Use cases in
api/ stay as plain Kotlin classes (no annotations).
- The
impl/ module provides them via @Provides in <Name>Module.kt for the mobile path.
- The web target instantiates use cases manually (
GetOrderHistoryUseCase(JsOrderHistoryRepository())) at the call site.
Default to "Metro everywhere" unless you have a concrete reason to opt out.
Consumer-side graph: two-graph wiring
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):
@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:
@DependencyGraph(
bindingContainers = [WebPlatformModule::class],
)
interface WebAppGraph {
val getOrderHistoryPageUseCase: GetOrderHistoryPageUseCase
val getOrderDetailUseCase: GetOrderDetailUseCase
val orderHistoryRepository: OrderHistoryRepository
}
@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.
Step 15: Settings Update
- Add both modules to
settings.gradle.kts
- Add impl dependency to
composeApp/build.gradle.kts
Phase 4: Validation Checklist
After generation, verify ALL of these:
Phase 5: Build Verification
./gradlew :feature:<name>:api:assemble
./gradlew :feature:<name>:impl:assemble
./gradlew :composeApp:assemble
Reference Files
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 |
Integration with Other Skills
| 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. |