Kotlin Multiplatform application with Compose UI for Android, iOS and Desktop.
- JDK 24 (Temurin recommended)
- Android Studio / IntelliJ IDEA
- Xcode (for iOS)
./gradlew :composeApp:run./gradlew :composeApp:installDebugOpen iosApp/iosApp.xcodeproj in Xcode and run.
./gradlew allTests./gradlew detekt./gradlew csCheck./gradlew csFix./gradlew tasks --group=verificationshared/ # Backend (business logic)
├── domain/ # Entities & Repository interfaces
│ ├── entity/
│ └── repository/
├── application/ # Actions (use cases)
│ └── action/
└── infrastructure/ # Implementations
├── config/ # DI Container & Bootstrap
├── http/ # HTTP client
└── repository/ # Repository implementations
composeApp/ # Frontend (UI)
└── ui/
├── layout/ # Base layouts (like Twig templates)
├── component/ # Reusable components
└── screen/ # Screens
package com.kmpboilerplate.application.action
class GetCatsAction(
private val repository: CatRepositoryInterface
) {
suspend operator fun invoke(limit: Int = 10): Result<List<Cat>> {
return try {
Result.success(repository.getCats(limit = limit))
} catch (e: Exception) {
Result.failure(e)
}
}
}// Domain - interface
package com.kmpboilerplate.domain.repository
interface CatRepositoryInterface {
suspend fun getRandomCat(): Cat
suspend fun getCats(limit: Int): List<Cat>
}
// Infrastructure - implementation
package com.kmpboilerplate.infrastructure.repository
class CatRepository(
private val client: HttpClient
) : CatRepositoryInterface {
override suspend fun getRandomCat(): Cat {
return client.get("$BASE_URL/cat?json=true").body()
}
override suspend fun getCats(limit: Int): List<Cat> {
return client.get("$BASE_URL/cats?limit=$limit").body()
}
}// infrastructure/config/Container.kt
val repositoryModule = module {
singleOf(::CatRepository) bind CatRepositoryInterface::class
}
val actionModule = module {
factoryOf(::GetCatsAction)
}
val container = listOf(networkModule, repositoryModule, actionModule)@Composable
fun CatScreen(
getCats: GetCatsAction = koinInject()
) {
var cats by remember { mutableStateOf<List<Cat>>(emptyList()) }
LaunchedEffect(Unit) {
getCats(limit = 20).onSuccess { cats = it }
}
AppLayout(title = "Cats") { padding ->
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
modifier = Modifier.padding(padding)
) {
items(cats) { cat ->
ImageTile(imageUrl = cat.imageUrl)
}
}
}
}@Composable
fun ImageTile(
imageUrl: String,
modifier: Modifier = Modifier,
badge: @Composable () -> Unit = {}
) {
Card(
modifier = modifier.aspectRatio(1f),
shape = RoundedCornerShape(16.dp)
) {
Box {
AsyncImage(
model = imageUrl,
contentScale = ContentScale.Crop
)
badge()
}
}
}This project enforces a strict coding standard using two tools:
- Zero tolerance policy (
maxIssues: 0) — any issue breaks the build - Dead code detection:
UnusedPrivateMember,UnusedPrivateClass,UnusedImports - Redundant code:
RedundantVisibilityModifierRule,UnnecessaryAbstractClass - Design rules:
UseDataClass,UtilityClassWithPublicConstructor - Compose rules via compose-rules plugin
- Config:
detekt.yml
- Automatic code formatting to a single standard (
ktlint_official) - Run
./gradlew csFixto auto-fix all formatting issues - Run
./gradlew csCheckto verify without modifying files - Config:
.editorconfig
Learn more at these links: