Kmp expect actual
Skill almasumdev/awesome-kotlin-multiplatform-agent-skills/.github/skills/architecture/kmp-expect-actual
Idiomatic use of `expect`/`actual` declarations in Kotlin Multiplatform, when to prefer interfaces + DI instead, and how to design platform abstractions that age well. Use when you need platform-specific behavior in `commonMain`.From its SKILL.md
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-expect-actualAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.3 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it
expect / actual (and when to avoid it)
Instructions
expect/actual is the KMP compiler-level mechanism for providing a platform-specific implementation of a declaration. It is one of several tools; it is often not the best one.
1. Decision tree
Do you need different behavior per platform?
├─ Is the API *exactly* the same shape on every target? ──► expect/actual
├─ Is it platform-specific infra (Context, NSUserDefaults, window)? ──► interface + DI
└─ Is it a value that differs but has no behavior? ──► expect val / BuildKonfig
Prefer an interface with a DI-provided platform implementation. It is testable (mock in commonTest), composable (wrap with decorators), and lives at the app layer, not the compiler layer.
2. expect/actual — the good cases
Small, stateless, truly language-level differences:
// commonMain
expect fun currentTimeMillis(): Long
expect class Uuid {
companion object { fun random(): Uuid }
override fun toString(): String
}
// androidMain
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual class Uuid private constructor(private val v: java.util.UUID) {
actual companion object {
actual fun random(): Uuid = Uuid(java.util.UUID.randomUUID())
}
actual override fun toString(): String = v.toString()
}
// iosMain
import platform.Foundation.NSDate
import platform.Foundation.NSUUID
actual fun currentTimeMillis(): Long =
(NSDate().timeIntervalSince1970 * 1000).toLong()
actual class Uuid private constructor(private val v: NSUUID) {
actual companion object {
actual fun random(): Uuid = Uuid(NSUUID())
}
actual override fun toString(): String = v.UUIDString
}
3. The DI-preferred cases
Anything that needs lifecycle, context, or state. Example: a secure key/value store.
// commonMain — interface only, no expect
interface SecureStore {
fun putString(key: String, value: String)
fun getString(key: String): String?
fun remove(key: String)
}
// androidMain
class AndroidSecureStore(context: Context) : SecureStore {
private val prefs = EncryptedSharedPreferences.create(
context, "secure", MasterKey.Builder(context).build(),
PrefKeyEncryptionScheme.AES256_SIV, PrefValueEncryptionScheme.AES256_GCM,
)
override fun putString(key: String, value: String) { prefs.edit().putString(key, value).apply() }
override fun getString(key: String): String? = prefs.getString(key, null)
override fun remove(key: String) { prefs.edit().remove(key).apply() }
}
// iosMain
import platform.Foundation.NSUserDefaults
class IosSecureStore : SecureStore {
private val defaults = NSUserDefaults.standardUserDefaults
override fun putString(key: String, value: String) { defaults.setObject(value, key) }
override fun getString(key: String): String? = defaults.stringForKey(key)
override fun remove(key: String) { defaults.removeObjectForKey(key) }
}
Wire it with Koin:
// commonMain
val coreModule = module { /* uses get<SecureStore>() */ }
// androidMain
actual fun platformModule() = module { single<SecureStore> { AndroidSecureStore(get()) } }
// iosMain
actual fun platformModule() = module { single<SecureStore> { IosSecureStore() } }
Only the factory function name is expect/actual:
// commonMain
expect fun platformModule(): Module
4. expect class rules (Kotlin 2.0)
expect classmay declareconstructor, members, and supertypes.actualclasses must match exactly (same members, same visibility, same supertypes).expectdeclarations cannot have bodies (exceptexpect companion object { }which may nestexpect fun).- Default parameter values: allowed on
expect, forbidden onactual(theexpectdefault wins). - As of Kotlin 2.0,
expect/actualclasses are stable but still emit a warning: prefer typealiases to existing JDK/Darwin types when possible.
5. typealias shortcut
If the platform already has the right type, skip the class:
// commonMain
expect class AtomicLong(initial: Long) {
fun addAndGet(delta: Long): Long
fun get(): Long
}
// androidMain / jvmMain
actual typealias AtomicLong = java.util.concurrent.atomic.AtomicLong
// iosMain
actual class AtomicLong actual constructor(initial: Long) {
private val backing = kotlin.native.concurrent.AtomicLong(initial)
actual fun addAndGet(delta: Long): Long = backing.addAndGet(delta)
actual fun get(): Long = backing.value
}
Checklist
- Used
interface + DIfor anything stateful or lifecycle-bound. - Used
expect/actualonly for stateless, value-like APIs. - No
actualdeclaration introduces new public members beyond theexpect. - No default values on
actualparameters. - Platform types are
typealiased to existing SDK types where possible. -
expectdeclarations live incommonMain, never in an intermediate source set unless that set owns all of its actuals.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.