Skip to content

Project Requirements

Ali Sadeghi edited this page Jan 27, 2026 · 7 revisions

Project Requirements

Architectural patterns and project structure for KMP features.

New to KMPilot? Start with Getting Started to understand features and the development workflow.

Directory Structure

YourProject/
├── composeApp/
│   └── src/commonMain/kotlin/{pkg}/
│       ├── initKoin.kt        # Must contain startKoin
│       └── BaseAppNavHost.kt  # Must contain NavHost
├── core/
│   ├── common/          # Either, UiState, BaseFeature, setState
│   ├── data/            # ApiClient, network layer
│   └── designsystem/    # X-components (XTheme, XButton, etc.)
└── feature/
    └── {featurename}/   # Feature modules (lowercase)
        ├── src/commonMain/kotlin/{pkg}/featurename/
        │   ├── data/
        │   │   ├── model/
        │   │   ├── remote/
        │   │   └── repository/
        │   └── presentation/
        │       ├── model/
        │       ├── viewmodel/
        │       ├── ui/
        │       └── navigation/
        └── src/commonTest/kotlin/

Required Patterns

Error Handling with Either

// ✅ Correct
suspend fun login(request: LoginRequest): Either<LoginResponse>

// ❌ Wrong
suspend fun login(request: LoginRequest): LoginResponse
suspend fun login(request: LoginRequest): Result<LoginResponse>

UI State with 4 States

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

// ❌ Wrong
sealed interface LoginUiState {
    data object Loading : LoginUiState
    data class Success(val token: String) : LoginUiState
}

State Updates with setState

// ✅ Correct
fun login(email: String, password: String) {
    viewModelScope.launch {
        setState { Loading }
        val result = repository.login(email, password)
        when (result) {
            is Success -> setState { Success(result.data) }
            is Failure -> setState { Failed(result.error) }
        }
    }
}

// ❌ Wrong
fun login(email: String, password: String) {
    _state.value = Loading
    viewModelScope.launch {
        val result = repository.login(email, password)
        _state.value = Success(result)
    }
}

DataSource with Interface + Impl

// ✅ Correct
interface ProductRemoteDataSource {
    suspend fun getProducts(): Either<List<Product>>
}

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

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

Repository with Interface + Impl

// ✅ Correct
interface ProductRepository {
    suspend fun getProducts(): Either<List<Product>>
}

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

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

X-Components in UI

// ✅ Correct
@Composable
fun LoginScreen() {
    XScaffold(title = { XText("Login") }) {
        XTextField(value = "", onValueChange = {})
        XButton(onClick = {}, text = "Login")
    }
}

// ❌ Wrong
@Composable
fun LoginScreen() {
    Scaffold(topBar = { TopAppBar(title = { Text("Login") }) }) {
        TextField(value = "", onValueChange = {})
        Button(onClick = {}) { Text("Login") }
    }
}

Type-Safe Navigation

// ✅ Correct - Routes contain only data, callbacks are passed in composable
@Serializable
data class ProductDetailRoute(val productId: String)

composable<ProductDetailRoute> { backStackEntry ->
    val route = backStackEntry.toRoute<ProductDetailRoute>()
    ProductDetailScreen(
        productId = route.productId,
        onBackClick = { navController.popBackStack() }  // Callback passed here
    )
}

// ❌ Wrong - Callbacks in route (not serializable)
@Serializable
data class ProductDetailRoute(
    val productId: String,
    val onBackClick: () -> Unit  // Not allowed!
)

// ❌ Wrong - Non-serializable sealed classes
sealed class Route {
    data class ProductDetail(val id: String) : Route()
}

Dependency Injection with Koin

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

// In initKoin.kt
modules(ProductModule().module)

// ❌ Wrong
fun setupProduct() {
    val dataSource = ProductRemoteDataSourceImpl(apiClient)
    val repository = ProductRepositoryImpl(dataSource)
    productViewModel = ProductListViewModel(repository)
}

ImmutableList for Collections

// ✅ Correct
data class ProductListUiState(
    val products: ImmutableList<ProductUiModel>
)

setState {
    Success(products.toImmutableList())
}

// ❌ Wrong
data class ProductListUiState(
    val products: List<ProductUiModel>
)

Serializable Models

// ✅ Correct
@Serializable
data class Product(
    val id: String,
    val name: String,
    val price: Double
)

// ❌ Wrong
data class Product(
    val id: String,
    val name: String,
    val price: Double
)

Ktor Resources (Type-Safe API Routes)

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

// Usage
val response = client.get(ProductResource())
val product = client.get(ProductResource.Id(ProductResource(), "123"))

// ❌ Wrong
val response = client.get("/products")
val product = client.get("/products/123")

Integration Checklist

After feature generation, verify these 4 integration points:

1. settings.gradle.kts

include(":feature:yourfeature")

2. composeApp/build.gradle.kts

dependencies {
    implementation(projects.feature.yourfeature)
}

3. initKoin.kt

fun initKoin() {
    startKoin {
        modules(
            YourFeatureModule().module
        )
    }
}

4. BaseAppNavHost.kt

@Composable
fun BaseAppNavHost(navController: NavHostController) {
    XNavHost(navController, startDestination = HomeRoute) {
        composable<YourFeatureRoute> {
            YourFeatureScreen(
                onBackClick = { navController.popBackStack() }
            )
        }
    }
}

Back to Home

Clone this wiki locally