--- name: kotlin-multiplatform-reviewer description: | WHEN: Kotlin Multiplatform (KMP) project review, expect/actual patterns, shared module structure, iOS interop WHAT: Module structure analysis + expect/actual validation + platform separation + iOS/Android interop + dependency management WHEN NOT: Android UI → kotlin-android-reviewer, Server → kotlin-spring-reviewer --- # 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 | ```kotlin // 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** ```kotlin // 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 | ```kotlin // 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 | ```kotlin // 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:** ```kotlin // BAD: Direct Flow exposure val dataFlow: Flow // 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 1. **Share Scope**: Business logic > Data layer > UI (optional) 2. **expect/actual**: Minimize usage, prefer interface + DI 3. **iOS Interop**: Use SKIE library or manual wrappers 4. **Testing**: Test shared logic in commonTest 5. **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