expect/actual — en mekanism i Kotlin Multiplatform som gör det möjligt att deklarera plattformsberoende API:er i gemensam kod. Nyckelordet expect skapar ett kontrakt för en funktion, klass eller egenskap i commonMain, medan nyckelordet actual tillhandahåller en konkret implementering för varje plattform. Kompilatorn kontrollerar att varje expect-deklaration har en motsvarande actual-implementering på alla målplattformar. Enligt JetBrains, 2025 används mekanismen i 80% av KMM-projekt för att implementera plattformsaffärslogik.
Huvudpunkter
expect/actual — är en deklarativ mekanism i Kotlin Multiplatform för plattformsorienterad programmering. Den gör det möjligt att beskriva ett API en gång i den gemensamma modulen (expect) och implementera det separat för varje plattform (actual). Till skillnad från gränssnitt skapar expect/actual inga virtuella anrop — kompilatorn kopplar samman expect- och actual-deklarationer vid kompileringstillfället, vilket eliminerar omkostnader för dynamisk dirigering.
Historien om expect/actual började med lanseringen av Kotlin Multiplatform 2017. Från början kallades mekanismen expect/actual declarations och var experimentell. I Kotlin 1.2 lades expect-annotationer till, och i Kotlin 1.3 blev expect/actual stabilt för klasser och funktioner. Gradvis utökades mekanismen: i Kotlin 1.6 lades stöd för expect/actual för companion-objekt till, i Kotlin 1.7 — för enum-klasser, och i Kotlin 2.0 — för typealias.
Den viktigaste egenskapen hos expect/actual är säkerhet på kompileringsnivå. Om en utvecklare lägger till en expect-deklaration i commonMain men glömmer att tillhandahålla en actual-implementering för iOS, ger kompilatorn ett fel. Detta förhindrar körningsfel som är typiska för metoder med reflektion eller dynamisk inläsning av plattformskod.
Mekanismen expect/actual arbetar på source set-nivå — Kotlin Multiplatforms modulsystem. Gemensam kod som är tillgänglig för alla plattformar finns i source set commonMain. Plattformsberoende kod — i iosMain, androidMain, macosMain och så vidare. Nyckelordet expect i commonMain deklarerar ett API, medan nyckelordet actual i plattformens source set tillhandahåller implementeringen. Kompilatorn kopplar samman dem vid kodgenerering och ersätter anropet till expect-funktionen med motsvarande actual-implementering för målplattformen.
Source set-hierarkin i ett typiskt KMM-projekt ser ut så här: commonMain innehåller expect-deklarationer, iosMain och androidMain innehåller actual-implementeringar. Vid kompilering för iOS används actual från iosMain, vid kompilering för Android — från androidMain. Source set kan vara mellanliggande (t.ex. iosArm64Main för en specifik arkitektur), vilket gör det möjligt att specificera implementeringar för olika enheter.
// commonMain — expect-deklaration
expect fun getPlatformName(): String
// androidMain — actual för Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual för iOS
actual fun getPlatformName(): String = "iOS"
Kotlin-kompilatorn kontrollerar flera villkor vid arbete med expect/actual. Varje expect-deklaration måste ha en actual-implementering för varje aktiv plattform. Signaturen för actual-deklarationen måste matcha expect-signaturen (annotationen @OptionalExpectation kan mildra detta krav). Åtkomstmodifierare, returtyp och parametrar måste vara identiska. Kompilatorn kontrollerar också frånvaron av cykliska beroenden mellan expect- och actual-deklarationer.
expect/actual stödjer flera typer av deklarationer. De vanligaste är expect/actual-funktioner för plattformsoperationer, expect/actual-klasser för objekt som kräver inbyggd implementering och expect/actual-egenskaper för konstanter och inställningar. Varje typ har sina egna användningsregler och begränsningar.
Expect/actual-funktioner — den enklaste och mest använda typen. De används för att anropa plattforms-API:er som att hämta tid, läsa filer eller skicka HTTP-förfrågningar. Expect/actual-klasser används för att skapa objekt som direkt interagerar med inbyggd kod (t.ex. för kameraåtkomst, geolokalisering eller säker lagring). Expect/actual-egenskaper (val) är lämpliga för plattformskonstanter — OS-namn, SDK-version eller sökväg till systemkatalog.
| Deklarationstyp | Nyckelord | Användningsexempel |
|---|---|---|
| Funktion | expect fun / actual fun | Hämta unik enhetsidentifierare |
| Klass | expect class / actual class | Åtkomst till SecureStorage (Keychain / EncryptedSharedPreferences) |
| Egenskap | expect val / actual val | Aktuell plattform (iOS / Android) |
| Enum-klass | expect enum / actual enum | Lista över tillgängliga appbehörigheter |
| Typealias | expect typealias / actual typealias | Plattformsspecifik nätverkssvartyp |
Alla Kotlin-konstruktioner kan inte användas med expect/actual. En expect-deklaration kan inte ha en kropp — endast signatur. En expect-klass kan inte ha en konstruktor med parametrar (måste ha en tom primärkonstruktor). För enum expect/actual måste alla konstanter vara desamma i expect och actual. Expect-egenskaper måste vara val (inte var), eftersom lagring av tillstånd i den gemensamma modulen för plattformsegenskaper inte är meningsfullt.
Låt oss titta på praktiska exempel på expect/actual från enkla funktioner till fullvärdiga klasser. Grundfallet — att hämta plattformsnamnet för användning i UI. Mer komplexa exempel inkluderar åtkomst till inbyggd lagring och arbete med plattformstrådar.
// commonMain — expect-klass för säker lagring
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual på Android
actual class PlatformStorage {
private val prefs = AppContext.getSharedPreferences("secure", 0)
actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
actual fun get(key: String): String? = prefs.getString(key, null)
actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}
I detta exempel definierar expect-klassen PlatformStorage ett kontrakt för en enkel nyckel-värde-lagring. På Android använder implementeringen SharedPreferences, på iOS — Keychain eller NSUserDefaults. Tack vare expect/actual anropar affärslogiken i commonMain save/get/remove utan att känna till plattformsimplementeringen.
// iosMain — actual på iOS med Keychain
actual class PlatformStorage {
actual fun save(key: String, value: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecValueData to value.encodeToByteArray()
)
SecItemAdd(query, null)
}
actual fun get(key: String): String? {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecReturnData to true
)
val result = mutableMapOf<String, Any>()
return if (SecItemCopyMatching(query, result) == errSecSuccess)
result[kSecValueData]?.toString()
else null
}
actual fun remove(key: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key
)
SecItemDelete(query)
}
}
Vid design av expect/actual-API:er bör flera principer följas. Minimera antalet expect-deklarationer — ju mer gemensam kod, desto enklare underhåll. Använd expect/actual endast för de API:er som verkligen skiljer sig mellan plattformar. För resten av koden använd gränssnitt med fabriker eller dependency injection, vilket förenklar testning.
Det rekommenderas att gruppera expect-deklarationer i tematiska moduler istället för att blanda dem i en fil. Till exempel Storage.kt för expect-deklarationer av lagring, Platform.kt för expect-funktioner för OS-arbete och Analytics.kt för expect-klasser för analys. Detta förenklar navigering och förståelse av plattformsytan i ett KMM-projekt. Varje actual-fil bör finnas i motsvarande source set: androidMain, iosMain, desktopMain och så vidare.
Standardimplementeringar via expect fun med actual fun där actual använder gemensam kod är ett vanligt anti-mönster. Om plattformsimplementeringen inte skiljer sig från standarden, behövs inte expect/actual. I sådana fall använd en enkel funktion i commonMain. Undvik också expect/actual för triviala getters — använd expect val med konstanter.
Korrekt struktur för expect/actual-kod är avgörande för projektets läsbarhet. Varje expect/actual-modul bör ha en enda ingångspunkt. Exempel på organisering: commonMain/kotlin/com/project/platform innehåller expect-deklarationer, androidMain/kotlin/com/project/platform — actual för Android, iosMain/kotlin/com/project/platform — actual för iOS. Fil- och paketnamn bör matcha för expect och actual så att utvecklaren snabbt kan hitta motsvarande implementering.
Gränssnitt med plattformsfabrik — det främsta alternativet till expect/actual. Istället för en expect-klass kan du deklarera ett gränssnitt i commonMain och skapa konkreta klasser i plattformsmoduler. En fabrik eller dependency injection-container tillhandahåller rätt implementering vid körning. Denna metod är bättre lämpad för testning eftersom gränssnittet kan mockas.
Dependency Injection (Koin, Kodein) — ett mer flexibelt men mindre prestandaeffektivt tillvägagångssätt. DI-containern konfigureras separat för varje plattform och tillhandahåller plattformsberoenden till gemensam kod. Till skillnad från expect/actual sker injicering vid körning, vilket gör det möjligt att byta ut implementeringar för testning. Å andra sidan upptäcks DI-konfigurationsfel först vid start, inte vid kompilering.
| Metod | Kontroll vid kompilering | Testflexibilitet | Körningsomkostnad |
|---|---|---|---|
| expect/actual | Fullständig | Låg (actual kan inte mockas) | Ingen (kompileringsbindning) |
| Gränssnitt + fabrik | Delvis | Hög (kan mockas) | Minimal (virtuellt anrop) |
| Dependency Injection | Nej (körning) | Hög | Medel (DI-proxy) |
Valet mellan expect/actual och alternativ beror på sammanhanget. För prestandakritiska tillämpningar (spelmotorer, realtidsbearbetning) är expect/actual att föredra på grund av noll omkostnad. För affärslogik (databaser, use-cases) är det bättre att använda gränssnitt med DI för att förenkla testning. En kombinerad metod — expect/actual för lågnivå plattformsoperationer och gränssnitt för affärslogiklagret — används i de flesta produktions-KMM-projekt.
Vanliga frågor
expect/actual binder implementeringen vid kompilering utan virtuella anrop, medan gränssnitt gör det vid körning. expect/actual garanterar att implementering finns för alla plattformar, gränssnitt kräver körningskontroller.
Ja, expect enum stöds från och med Kotlin 1.7. Alla konstanter i expect och actual enum måste vara identiska. Olika värden på konstanter på olika plattformar är ett kompileringsfel.
Kompilatorn ger ett fel för varje plattform där actual-implementering saknas. Projektet kommer inte att byggas förrän alla expect-deklarationer har motsvarande actual-implementeringar.
Nej, expect och actual måste finnas i olika source set. expect — i commonMain eller ett mellanliggande source set, actual — i plattformens source set. Att placera expect och actual i samma source set är ett kompileringsfel.
För att testa expect/actual, använd commonTest med plattformsspecifika test-source set. Skriv expect-tester i commonTest och actual-tester för varje plattform. Integrationstester körs separat på varje målplattform.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också