-
Notifications
You must be signed in to change notification settings - Fork 0
Creating KMP Feature
Ali Sadeghi edited this page Feb 5, 2026
·
6 revisions
Auto-activates when: User mentions "create feature", "new module", "add feature"
New to features? See Getting-Started to understand what "features" are in KMPilot.
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)
> Create settings feature with toggle switches for notifications and dark modeGenerates: ViewModel, Screen, Navigation (no DataSource/Repository)
> Create product catalog with list, search, filters, and detail screensGenerates: Models, Ktor Resources, DataSource, Repository, UiModels, ViewModels, Screens, Navigation
> Create login feature with email/password and OAuth supportGenerates: LoginRequest/Response models, AuthRepository, LoginViewModel, LoginScreen, token storage
> 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,BookDetailUiModelwith formatted data -
ViewModels:
BookListViewModel,BookDetailViewModelwith search/filter state -
Screens:
BookListScreenwith search bar, filters, and grid/list toggle;BookDetailScreenwith scrollable content - Navigation: Type-safe routes for list and detail screens
- Tests: Complete test coverage for all layers
// 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()
}// 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)// 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