Skip to content

Project Requirements

Ali Sadeghi edited this page May 18, 2026 · 7 revisions

Project Requirements

The architectural rules every feature in KMPilot must follow. This is the enforceable layer — the code-reviewer agent (and the reinject-on-compact.sh hook) check these.

New to KMPilot? Start with Getting-Started.

Directory Structure

KMPilot/
├── composeApp/
│   └── src/commonMain/kotlin/{pkg}/kmpilot/
│       ├── initKoin.kt          # contains startKoin + feature initializers
│       └── BaseAppNavHost.kt    # contains the NavHost + calls to NavGraphBuilder extensions
├── core/
│   ├── common/                  # Either, UiState, BaseFeature, setState, ErrorModel
│   ├── data/                    # ApiClient, ErrorConst, network layer
│   └── designsystem/            # X-components (XTheme, XButton, …)
└── feature/
    └── {featurename}/           # lowercase, no hyphens/underscores
        ├── build.gradle.kts
        └── src/commonMain/kotlin/{pkg}/{featurename}/
            ├── data/
            │   ├── model/
            │   ├── remote/
            │   ├── datasource/
            │   └── repository/
            ├── presentation/
            │   ├── {Feature}ViewModel.kt    # flat — no viewmodel/ subdir
            │   ├── {Feature}UiState.kt
            │   ├── {Feature}UiModel.kt
            │   ├── ui/
            │   │   ├── {Feature}Screen.kt   # Screen + ScreenRoot + state routing
            │   │   └── components/          # self-contained UI units
            │   └── navigation/
            └── di/
                └── {Feature}Modules.kt

10 Critical Rules

1. Interface + Impl

//
interface ProductRemoteDataSource {
    suspend fun getProducts(): Either<List<Product>>
}
class ProductRemoteDataSourceImpl(...) : ProductRemoteDataSource { ... }

//
class ProductRemoteDataSource(...) { ... }

2. Either<T> for fallible operations — never throw

//
suspend fun login(req: LoginRequest): Either<LoginResponse>

//
suspend fun login(req: LoginRequest): LoginResponse
suspend fun login(req: LoginRequest): Result<LoginResponse>

3. setState { copy() }

//
_uiState.setState { copy(isLoading = true) }

//
_uiState.value = _uiState.value.copy(isLoading = true)

4. 4 UI States — Uninitialized / Loading / Success / Failed

//
sealed interface LoginUiState : UiState {
    data object Uninitialized : LoginUiState
    data object Loading       : LoginUiState
    data class  Success(val token: String) : LoginUiState
    data class  Failed(val error: ErrorModel) : LoginUiState
}

5. X-components only in features (no Material3)

//
XScaffold(title = { XText("Login") }) {
    XTextField(value = "", onValueChange = {})
    XButton(onClick = {}, text = "Login")
}

//
Scaffold(topBar = { TopAppBar(title = { Text("Login") }) }) {
    TextField(...); Button(...) { Text(...) }
}

MaterialTheme.colorScheme and MaterialTheme.typography are allowed (XTheme wraps MaterialTheme). Only component imports are forbidden.

6. ImmutableList for collections

//
data class ProductListUiState(val products: ImmutableList<ProductUiModel>)
setState { copy(products = products.toImmutableList()) }

7. Lowercase package names

//
package thisissadeghi.productdetail

// ❌  thisissadeghi.product-detail
// ❌  thisissadeghi.productDetail
// ❌  thisissadeghi.product_detail

8. DI Pattern — object … : BaseFeature with initialize()

object DashboardModules : BaseFeature(DashboardModules::class.simpleName.toString()) {
    override fun getKoinModules(): List<Module> =
        listOf(
            module {
                singleOf(::DashboardRemoteDataSourceImpl).bind<DashboardRemoteDataSource>()
                singleOf(::DashboardRepositoryImpl).bind<DashboardRepository>()
                viewModelOf(::DashboardViewModel)
            },
        )

    override fun initialize() { DashboardModules }
}

Registration in initKoin.kt:

private fun initializeFeatures() {
    CommonModules.initialize()
    DataModules.initialize()
    DashboardModules.initialize()
    // ... etc.
}

9. No UseCases

ViewModels invoke repositories directly. No UseCase / Interactor layer.

10. Callback parameters in Screens (not navController)

//
@Composable
fun DashboardScreen(
    viewModel: DashboardViewModel = koinViewModel(),
    onActionClick: (String) -> Unit,
    onBackToDashboard: () -> Unit,
) { ... }

//
@Composable
fun DashboardScreen(navController: NavController) { ... }

4 Integration Points

Every new feature wires through exactly these 4:

// 1. settings.gradle.kts
include(":feature:dashboard")

// 2. composeApp/build.gradle.kts
dependencies {
    implementation(project(":feature:dashboard"))
}

// 3. initKoin.kt
private fun initializeFeatures() {
    // ...
    DashboardModules.initialize()
}

// 4. BaseAppNavHost.kt
XNavHost(navController = navController, startDestination = DashboardRoute) {
    dashboard(
        onActionClick = { id -> /* ... */ },
        onBackToDashboard = { navController.popBackStack(DashboardRoute, false) },
    )
}

Navigation Pattern (Type-Safe + Callbacks)

Routes contain only data; callbacks live in the NavGraphBuilder.{featurename}() extension:

@Serializable
data object DashboardRoute

fun NavGraphBuilder.dashboard(
    onActionClick: (String) -> Unit,
    onBackToDashboard: () -> Unit,
) {
    composable<DashboardRoute> {
        DashboardScreen(
            viewModel = koinViewModel(),
            onActionClick = onActionClick,
            onBackToDashboard = onBackToDashboard,
        )
    }
}

BaseAppNavHost.kt just calls dashboard(...) — never composable<Route> { ... } directly.

Module Dependencies

A feature depends on When
:core:common Always (Either, UiState, setState, ErrorModel)
:core:designsystem Always (X-components)
:core:data Only if using ApiClient

Features NEVER depend on other features.

Ktor Resources (Type-Safe API Routes)

//
@Resource("/products")
class ProductResource {
    @Resource("{id}") data class Id(val parent: ProductResource, val id: String)
}
val list   = client.get(ProductResource())
val single = client.get(ProductResource.Id(ProductResource(), "123"))

//
client.get("/products")
client.get("/products/123")

ScreenRoot Pattern

// Screen — ViewModel wrapper (NOT tested)
@Composable
fun FeatureScreen(viewModel: FeatureViewModel, onBackClick: () -> Unit) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    FeatureScreenRoot(uiState, onBackClick, viewModel::retry)
}

// ScreenRoot — ViewModel-independent (TESTABLE)
@Composable
fun FeatureScreenRoot(uiState: UiState, onBackClick: () -> Unit, onRetry: () -> Unit) {
    // X-components only
}

UI tests target ScreenRoot. See Test-UI.

Where to Find the Source of Truth

The single canonical file is .claude/skills/_shared/patterns.md. Every skill and agent imports it. This wiki page mirrors that file in a friendlier format — when in doubt, the patterns.md file wins.


Back to Home

Clone this wiki locally