Skip to content

Creating KMP Feature

Ali Sadeghi edited this page Feb 5, 2026 · 6 revisions

Creating KMP Feature Skill

Auto-activates when: User mentions "create feature", "new module", "add feature"

New to features? See Getting-Started to understand what "features" are in KMPilot.

Complete Feature Generation Workflow

Phase 0: Context Discovery (AUTO)
├── Detect PKG_PREFIX from feature/*/build.gradle.kts
├── Find initKoin.kt path (contains startKoin)
├── Find BaseAppNavHost.kt path (contains NavHost)
└── Detect core module packages

Phase 1: PRD Generation
├── Analyze user prompt
├── Generate Product Requirements Document
├── Save to .claude/docs/{feature}/prd.txt
└── ⏸️ Wait for user approval

Phase 2: Task Generation
├── Break PRD into implementation tasks
├── Assign tasks to agents (data, ui, integration)
├── Save to .claude/docs/{feature}/tasks.md
└── ⏸️ Wait for user approval

Phase 3: Implementation (Parallel)
├── 🔧 data-layer-agent (runs in parallel)
│   ├── Create models with @Serializable
│   ├── Create Ktor Resources (type-safe routes)
│   ├── Implement RemoteDataSource (interface + impl)
│   ├── Implement Repository (interface + impl)
│   └── Validate build
│
├── 🎨 ui-layer-agent (runs in parallel)
│   ├── Create UiModel (presentation models)
│   ├── Implement ViewModel with 4-state pattern
│   ├── Create Composable Screens with X-components
│   ├── Setup Navigation with type-safe routes
│   └── Validate build
│
└── 🔗 integration-agent (runs after data + ui)
    ├── Create DI module with Koin
    ├── Add to settings.gradle.kts
    ├── Add dependency to composeApp
    ├── Register in initKoin.kt
    ├── Wire navigation in BaseAppNavHost.kt
    ├── Generate living specification
    └── Validate full build

Phase 4: Cleanup
├── Verify spec.md exists
├── Remove prd.txt (ephemeral)
├── Remove tasks.md (ephemeral)
└── Remove task-*.md files (ephemeral)

Examples

Simple Feature (UI-only, no API)

> Create settings feature with toggle switches for notifications and dark mode

Generates: ViewModel, Screen, Navigation (no DataSource/Repository)

Complex Feature (API + multiple screens)

> Create product catalog with list, search, filters, and detail screens

Generates: Models, Ktor Resources, DataSource, Repository, UiModels, ViewModels, Screens, Navigation

Feature with Authentication

> Create login feature with email/password and OAuth support

Generates: LoginRequest/Response models, AuthRepository, LoginViewModel, LoginScreen, token storage

Complete Feature with API Details

> Create a book library feature with the following:
>
> Screens:
> - Book list screen with search and filters
> - Book detail screen with description and reviews
>
> API Endpoints:
>
> 1. GET /api/books?search={query}&genre={genre}
>    Response: {
>      "books": [
>        {
>          "id": "book-123",
>          "title": "Clean Code",
>          "author": "Robert Martin",
>          "genre": "Programming",
>          "coverUrl": "https://example.com/cover.jpg",
>          "rating": 4.5,
>          "publishYear": 2008
>        }
>      ],
>      "total": 42
>    }
>
> 2. GET /api/books/{id}
>    Response: {
>      "id": "book-123",
>      "title": "Clean Code",
>      "author": "Robert Martin",
>      "genre": "Programming",
>      "description": "A handbook of agile software craftsmanship...",
>      "coverUrl": "https://example.com/cover.jpg",
>      "rating": 4.5,
>      "reviewCount": 1250,
>      "publishYear": 2008,
>      "isbn": "978-0132350884"
>    }
>
> 3. POST /api/books/{id}/favorite
>    Request: { "isFavorite": true }
>    Response: { "success": true }

Generates:

  • Models: Book, BookDetail, BooksResponse, FavoriteRequest, FavoriteResponse
  • Ktor Resources: BookResource, BookResource.Id, BookResource.Favorite
  • DataSource: Interface + Impl with all three endpoints
  • Repository: Interface + Impl with domain logic
  • UiModels: BookUiModel, BookDetailUiModel with formatted data
  • ViewModels: BookListViewModel, BookDetailViewModel with search/filter state
  • Screens: BookListScreen with search bar, filters, and grid/list toggle; BookDetailScreen with scrollable content
  • Navigation: Type-safe routes for list and detail screens
  • Tests: Complete test coverage for all layers

What Gets Generated

Data Layer

// Models
@Serializable
data class Product(
    val id: String,
    val name: String,
    val price: Double,
    val imageUrl: String?
)

// Ktor Resources
@Resource("/products")
class ProductResource {
    @Resource("{id}")
    data class Id(val parent: ProductResource, val id: String)
}

// DataSource
interface ProductRemoteDataSource {
    suspend fun getProducts(): Either<List<Product>>
    suspend fun getProduct(id: String): Either<Product>
}

class ProductRemoteDataSourceImpl(
    private val client: ApiClient
) : ProductRemoteDataSource {
    override suspend fun getProducts(): Either<List<Product>> =
        client.get(ProductResource())
}

// Repository
interface ProductRepository {
    suspend fun getProducts(): Either<List<Product>>
    suspend fun getProduct(id: String): Either<Product>
}

class ProductRepositoryImpl(
    private val dataSource: ProductRemoteDataSource
) : ProductRepository {
    override suspend fun getProducts(): Either<List<Product>> =
        dataSource.getProducts()
}

UI Layer

// UiModel
data class ProductUiModel(
    val id: String,
    val name: String,
    val price: String, // Formatted: "$99.99"
    val imageUrl: String?
)

// ViewModel
class ProductListViewModel(
    private val repository: ProductRepository
) : ViewModel() {
    private val _state = MutableStateFlow<ProductListUiState>(Uninitialized)
    val state: StateFlow<ProductListUiState> = _state.asStateFlow()

    fun loadProducts() {
        viewModelScope.launch {
            setState { Loading }
            when (val result = repository.getProducts()) {
                is Success -> setState {
                    Success(result.data.map { it.toUiModel() })
                }
                is Failure -> setState { Failed(result.error) }
            }
        }
    }
}

// Screen
@Composable
fun ProductListScreen(
    viewModel: ProductListViewModel = koinViewModel(),
    onProductClick: (String) -> Unit
) {
    val state by viewModel.state.collectAsState()

    XScaffold(
        title = { XText("Products") }
    ) {
        when (val currentState = state) {
            is Uninitialized -> LaunchedEffect(Unit) {
                viewModel.loadProducts()
            }
            is Loading -> XLoadingIndicator()
            is Success -> ProductList(
                products = currentState.products,
                onProductClick = onProductClick
            )
            is Failed -> XErrorView(error = currentState.error)
        }
    }
}

// Navigation (routes contain only data, callbacks passed in composable)
@Serializable
data object ProductListRoute

@Serializable
data class ProductDetailRoute(val productId: String)

Integration

// DI Module
class ProductModule : BaseFeature {
    override fun Module.install() {
        single<ProductRepository> { ProductRepositoryImpl(get()) }
        single<ProductRemoteDataSource> { ProductRemoteDataSourceImpl(get()) }
        viewModel { ProductListViewModel(get()) }
    }
}

// initKoin.kt (auto-registered)
modules(ProductModule().module)

// BaseAppNavHost.kt (auto-wired)
composable<ProductListRoute> {
    ProductListScreen(
        onProductClick = { id -> navController.navigate(ProductDetailRoute(id)) }
    )
}

Back to Skills

Clone this wiki locally