Kotlin Multiplatform Reviewer Skill
Purpose
Reviews Kotlin Multiplatform (KMP) project structure and patterns including shared code design, expect/actual mechanism, and iOS interop.
When to Use
- KMP project code review
- "expect/actual", "shared module", "commonMain", "multiplatform" mentions
- iOS/Android code sharing design review
- Projects with
kotlin("multiplatform") plugin
Project Detection
kotlin("multiplatform") plugin in build.gradle.kts
src/commonMain, src/androidMain, src/iosMain directories
shared or common module exists
Workflow
Step 1: Analyze Structure
**Kotlin**: 2.0.x
**Targets**: Android, iOS (arm64, simulatorArm64)
**Shared Module**: shared
**Source Sets**:
- commonMain (shared code)
- androidMain (Android specific)
- iosMain (iOS specific)
Step 2: Select Review Areas
AskUserQuestion:
"Which areas to review?"
Options:
- Full KMP pattern check (recommended)
- Module structure/dependencies
- expect/actual implementation
- Platform code separation
- iOS interop (Swift/ObjC)
multiSelect: true
Detection Rules
Module Structure
| Check |
Recommendation |
Severity |
| Bloated shared module |
Split by layer |
MEDIUM |
| Circular dependencies |
Unidirectional deps |
HIGH |
| Platform code in commonMain |
Move to androidMain/iosMain |
HIGH |
| Missing test module |
Add commonTest |
MEDIUM |
Recommended Structure:
project/
├── shared/
│ └── src/
│ ├── commonMain/kotlin/ # Shared business logic
│ ├── commonTest/kotlin/ # Shared tests
│ ├── androidMain/kotlin/ # Android specific
│ ├── iosMain/kotlin/ # iOS specific
│ └── iosTest/kotlin/
├── androidApp/ # Android app
└── iosApp/ # iOS app (Xcode)
expect/actual Patterns
| Check |
Recommendation |
Severity |
| actual without expect |
expect declaration required |
CRITICAL |
| Missing actual impl |
Provide actual for all targets |
CRITICAL |
| Excessive expect/actual |
Consider interface + DI |
MEDIUM |
| Direct platform API in actual |
Add abstraction layer |
MEDIUM |
// commonMain - expect declaration
expect class Platform() {
val name: String
fun getDeviceId(): String
}
// androidMain - actual implementation
actual class Platform actual constructor() {
actual val name: String = "Android ${Build.VERSION.SDK_INT}"
actual fun getDeviceId(): String = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
}
// iosMain - actual implementation
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName()
actual fun getDeviceId(): String = UIDevice.currentDevice
.identifierForVendor?.UUIDString ?: ""
}
BAD: expect/actual overuse
// BAD: expect/actual for simple values
expect val platformName: String
actual val platformName: String = "Android"
// GOOD: interface + DI
interface PlatformInfo {
val name: String
}
// androidMain
class AndroidPlatformInfo : PlatformInfo {
override val name = "Android"
}
Platform Separation
| Check |
Recommendation |
Severity |
| Platform import in commonMain |
Move to platform source set |
CRITICAL |
| Java class in commonMain |
expect/actual or pure Kotlin |
HIGH |
| UIKit/Android SDK in common |
Separate to platform source set |
CRITICAL |
// BAD: Android import in commonMain
// commonMain/kotlin/Repository.kt
import android.content.Context // Compile error!
// GOOD: expect/actual separation
// commonMain
expect class DataStore {
fun save(key: String, value: String)
fun get(key: String): String?
}
// androidMain
actual class DataStore(private val context: Context) {
private val prefs = context.getSharedPreferences("app", Context.MODE_PRIVATE)
actual fun save(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
actual fun get(key: String): String? = prefs.getString(key, null)
}
// iosMain
actual class DataStore {
actual fun save(key: String, value: String) {
NSUserDefaults.standardUserDefaults.setObject(value, key)
}
actual fun get(key: String): String? =
NSUserDefaults.standardUserDefaults.stringForKey(key)
}
iOS Interop
| Check |
Recommendation |
Severity |
| Missing @ObjCName |
Swift-friendly naming |
LOW |
| Sealed class iOS exposure |
Use enum or @ObjCName |
MEDIUM |
| Direct Flow exposure to iOS |
Provide wrapper function |
HIGH |
| suspend function iOS call |
Provide completion handler wrapper |
HIGH |
// BAD: Direct suspend function exposure
class Repository {
suspend fun fetchData(): Data // Hard to call from iOS
}
// GOOD: iOS wrapper provided
class Repository {
suspend fun fetchData(): Data
// iOS completion handler wrapper
fun fetchDataAsync(completion: (Data?, Error?) -> Unit) {
MainScope().launch {
try {
val data = fetchData()
completion(data, null)
} catch (e: Exception) {
completion(null, e)
}
}
}
}
Flow iOS Exposure:
// BAD: Direct Flow exposure
val dataFlow: Flow<Data>
// GOOD: iOS wrapper
fun observeData(onEach: (Data) -> Unit): Closeable {
val job = MainScope().launch {
dataFlow.collect { onEach(it) }
}
return object : Closeable {
override fun close() { job.cancel() }
}
}
Dependency Management
| Check |
Recommendation |
Severity |
| Platform library in commonMain |
Use multiplatform library |
HIGH |
| Version mismatch |
Use Version Catalog |
MEDIUM |
| Unused dependencies |
Remove unused |
LOW |
Multiplatform Library Recommendations:
| Purpose |
Library |
| HTTP |
Ktor Client |
| Serialization |
Kotlinx Serialization |
| Async |
Kotlinx Coroutines |
| DI |
Koin, Kodein |
| Date/Time |
Kotlinx Datetime |
| Settings |
Multiplatform Settings |
| Logging |
Napier, Kermit |
| DB |
SQLDelight |
Response Template
## KMP Project Review Results
**Project**: [name]
**Kotlin**: 2.0.x
**Targets**: Android, iOS (arm64, simulatorArm64)
### Module Structure
| Status | Item | Issue |
|--------|------|-------|
| OK | Source set separation | commonMain/androidMain/iosMain correct |
| MEDIUM | Tests | Add commonTest recommended |
### expect/actual
| Status | File | Issue |
|--------|------|-------|
| OK | Platform.kt | expect/actual correctly implemented |
| HIGH | DataStore.kt | Missing iosMain actual implementation |
### iOS Interop
| Status | Item | Issue |
|--------|------|-------|
| HIGH | Repository.kt | suspend function needs iOS wrapper |
| MEDIUM | UiState.kt | Add @ObjCName to sealed class |
### Recommended Actions
1. [ ] Add DataStore iosMain actual implementation
2. [ ] Add completion handler wrapper to fetchData()
3. [ ] Add commonTest source set
Best Practices
- Share Scope: Business logic > Data layer > UI (optional)
- expect/actual: Minimize usage, prefer interface + DI
- iOS Interop: Use SKIE library or manual wrappers
- Testing: Test shared logic in commonTest
- Dependencies: Prefer multiplatform libraries
Integration
kotlin-android-reviewer skill: Android specific code
kotlin-spring-reviewer skill: Server shared code
code-reviewer skill: General code quality
Notes
- Based on Kotlin 2.0+
- KMP 1.9.20+ recommended (Stable)
- Compose Multiplatform requires separate review