agentsclimarketplace

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

Install
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-expect-actual

Assembled 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 class may declare constructor, members, and supertypes. actual classes must match exactly (same members, same visibility, same supertypes).
  • expect declarations cannot have bodies (except expect companion object { } which may nest expect fun).
  • Default parameter values: allowed on expect, forbidden on actual (the expect default wins).
  • As of Kotlin 2.0, expect/actual classes 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 + DI for anything stateful or lifecycle-bound.
  • Used expect/actual only for stateless, value-like APIs.
  • No actual declaration introduces new public members beyond the expect.
  • No default values on actual parameters.
  • Platform types are typealiased to existing SDK types where possible.
  • expect declarations live in commonMain, 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.

Keep looking

Skills are one crate of 326,696. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.