-
Notifications
You must be signed in to change notification settings - Fork 0
Project Requirements
Ali Sadeghi edited this page Jan 27, 2026
·
7 revisions
Architectural patterns and project structure for KMP features.
New to KMPilot? Start with Getting Started to understand features and the development workflow.
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/
// ✅ Correct
suspend fun login(request: LoginRequest): Either<LoginResponse>
// ❌ Wrong
suspend fun login(request: LoginRequest): LoginResponse
suspend fun login(request: LoginRequest): Result<LoginResponse>// ✅ 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
}// ✅ 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)
}
}// ✅ 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())
}// ✅ 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()
}// ✅ 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") }
}
}// ✅ 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()
}// ✅ 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)
}// ✅ Correct
data class ProductListUiState(
val products: ImmutableList<ProductUiModel>
)
setState {
Success(products.toImmutableList())
}
// ❌ Wrong
data class ProductListUiState(
val products: List<ProductUiModel>
)// ✅ 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
)// ✅ 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")After feature generation, verify these 4 integration points:
include(":feature:yourfeature")dependencies {
implementation(projects.feature.yourfeature)
}fun initKoin() {
startKoin {
modules(
YourFeatureModule().module
)
}
}@Composable
fun BaseAppNavHost(navController: NavHostController) {
XNavHost(navController, startDestination = HomeRoute) {
composable<YourFeatureRoute> {
YourFeatureScreen(
onBackClick = { navController.popBackStack() }
)
}
}
}Back to Home