expect/actual — het Kotlin Multiplatform-mechanisme waarmee platformafhankelijke API’s in gemeenschappelijke code kunnen worden gedeclareerd. Het sleutelwoord expect creëert een contract van een functie, klasse of eigenschap in commonMain, en het sleutelwoord actual levert een concrete implementatie voor elk platform. De compiler controleert of elke expect-declaratie overeenkomt met een actual-implementatie op alle doelplatforms. Volgens JetBrains, 2025, wordt het mechanisme gebruikt in 80% van de KMM-projecten voor het implementeren van platform-specifieke bedrijfslogica.
Belangrijkste punten
expect/actual — is een declaratief Kotlin Multiplatform-mechanisme voor platformgericht programmeren. Het maakt het mogelijk om API één keer in de gemeenschappelijke module (expect) te beschrijven en afzonderlijk voor elk platform (actual) te implementeren. In tegenstelling tot interfaces creëert expect/actual geen virtuele aanroepen — de compiler verbindt expect- en actual-declaraties in de compilatiefase, wat de overhead van dynamische dispatch elimineert.
De geschiedenis van expect/actual begon met de komst van Kotlin Multiplatform in 2017. Aanvankelijk heette het mechanisme expect/actual declarations en was het experimenteel. In Kotlin 1.2 werden expect-annotaties toegevoegd, en in Kotlin 1.3 werd expect/actual stabiel voor klassen en functies. Geleidelijk werd het mechanisme uitgebreid: in Kotlin 1.6 werd ondersteuning voor companion-objecten toegevoegd, in Kotlin 1.7 — voor enum-klassen, en in Kotlin 2.0 — voor typealias.
Het belangrijkste kenmerk van expect/actual — veiligheid op compilatieniveau. Als een ontwikkelaar een expect-declaratie aan commonMain heeft toegevoegd maar vergeet een actual-implementatie voor iOS te leveren, geeft de compiler een foutmelding. Dit voorkomt runtime-fouten die kenmerkend zijn voor benaderingen met reflectie of dynamisch laden van platformcode.
Het mechanisme expect/actual werkt op het niveau van source set — het modulesysteem van Kotlin Multiplatform. Gemeenschappelijke code die voor alle platforms toegankelijk is, bevindt zich in de source set commonMain. Platformafhankelijke code — in iosMain, androidMain, macosMain enzovoort. Het sleutelwoord expect in commonMain declareert de API, en het sleutelwoord actual in de platform source set levert de implementatie. De compiler verbindt ze in de codegeneratiefase, waarbij de aanroep van de expect-functie wordt vervangen door de overeenkomstige actual-implementatie voor het doelplatform.
De source set-hiërarchie in een typisch KMM-project ziet er als volgt uit: commonMain bevat de expect-declaraties, iosMain en androidMain bevatten de actual-implementaties. Bij compilatie voor iOS wordt actual uit iosMain gebruikt, bij compilatie voor Android — uit androidMain. Source sets kunnen intermediair zijn (bijvoorbeeld iosArm64Main voor een specifieke architectuur), wat het mogelijk maakt om implementaties voor verschillende apparaten te verfijnen.
// commonMain — expect-declaratie
expect fun getPlatformName(): String
// androidMain — actual voor Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual voor iOS
actual fun getPlatformName(): String = "iOS"
De Kotlin-compiler controleert verschillende voorwaarden bij het werken met expect/actual. Elke expect-declaratie moet een actual-implementatie hebben voor elk actief platform. De handtekening van de actual-declaratie moet overeenkomen met de expect-handtekening (de @OptionalExpectation-annotatie kan deze vereiste versoepelen). Toegangsmodifiers, retourtype en parameters moeten identiek zijn. De compiler controleert ook de afwezigheid van cyclische afhankelijkheden tussen expect- en actual-declaraties.
expect/actual ondersteunt verschillende typen declaraties. De meest gebruikte zijn expect/actual-functies voor platformbewerkingen, expect/actual-klassen voor objecten die een native implementatie vereisen, en expect/actual-eigenschappen voor constanten en instellingen. Elk type heeft zijn eigen gebruiksregels en beperkingen.
Expect/actual-functies — het eenvoudigste en meest voorkomende type. Ze worden gebruikt voor het aanroepen van platform-API’s zoals het ophalen van tijd, het lezen van bestanden of het verzenden van HTTP-verzoeken. Expect/actual-klassen worden toegepast voor het maken van objecten die rechtstreeks met native code interageren (bijvoorbeeld voor toegang tot camera, geolocatie of sleutelopslag). Expect/actual-eigenschappen (val) zijn geschikt voor platformconstanten — besturingssysteemnaam, SDK-versie of pad naar de systeemmap.
| Declaratietype | Sleutelwoorden | Voorbeeld gebruik |
|---|---|---|
| Functie | expect fun / actual fun | Unieke apparaat-ID ophalen |
| Klasse | expect class / actual class | Toegang tot SecureStorage (Keychain / EncryptedSharedPreferences) |
| Eigenschap | expect val / actual val | Huidig platform (iOS / Android) |
| Enum-klasse | expect enum / actual enum | Lijst met beschikbare app-machtigingen |
| Typealias | expect typealias / actual typealias | Platform-specifiek netwerkantwoordtype |
Niet alle Kotlin-constructies kunnen met expect/actual worden gebruikt. Een expect-declaratie mag geen body bevatten — alleen een handtekening. Een expect-klasse mag geen constructor met parameters hebben (moet een lege primary constructor hebben). Voor enum expect/actual moeten alle constanten identiek zijn in expect en actual. Expect-eigenschappen moeten val zijn (niet var), omdat het opslaan van status in de gemeenschappelijke module voor platformeigenschappen geen zin heeft.
Laten we praktische voorbeelden van expect/actual bekijken, van eenvoudige functies tot volwaardige klassen. Het basisgeval — het ophalen van de platformnaam voor gebruik in de UI. Complexere voorbeelden omvatten toegang tot native opslag en werken met platform threads.
// commonMain — expect-klasse voor veilige opslag
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual op 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() }
}
In dit voorbeeld definieert de expect-klasse PlatformStorage het contract van een eenvoudige sleutel-waardeopslag. Op Android gebruikt de implementatie SharedPreferences, en op iOS — Keychain of NSUserDefaults. Dankzij expect/actual roept de bedrijfslogica in commonMain save/get/remove aan zonder kennis van de platformimplementatie.
// iosMain — actual op iOS met 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)
}
}
Bij het ontwerpen van een expect/actual API moeten verschillende principes worden gevolgd. Minimaliseer het aantal expect-declaraties — hoe meer gemeenschappelijke code, hoe eenvoudiger het onderhoud. Gebruik expect/actual alleen voor API’s die echt verschillen tussen platforms. Voor de rest van de code pas je interfaces met fabrieken of dependency injection toe, wat testen vereenvoudigt.
Het wordt aanbevolen om expect-declaraties te groeperen op thematische modules, in plaats van ze in één bestand te mengen. Bijvoorbeeld Storage.kt voor expect-declaraties van opslag, Platform.kt voor expect-functies voor het werken met het besturingssysteem en Analytics.kt voor expect-klassen voor analyse. Dit vereenvoudigt navigatie en begrip van het platformoppervlak van het KMM-project. Elk actual-bestand moet zich in de overeenkomstige source set bevinden: androidMain, iosMain, desktopMain, enzovoort.
Standaardimplementaties via expect fun met actual fun, waarbij actual gemeenschappelijke code gebruikt — is een veelvoorkomend antipatroon. Als de platformimplementatie niet verschilt van de standaard, is expect/actual niet nodig. Gebruik in dergelijke gevallen een eenvoudige functie in commonMain. Vermijd ook expect/actual voor triviale getters — gebruik expect val met constanten.
De juiste structuur van expect/actual-code is cruciaal voor de leesbaarheid van het project. Elke expect/actual-module moet één enkel toegangspunt hebben. Voorbeeld van organisatie: commonMain/kotlin/com/project/platform bevat expect-declaraties, androidMain/kotlin/com/project/platform — actual voor Android, iosMain/kotlin/com/project/platform — actual voor iOS. Bestands- en pakketnamen moeten overeenkomen voor expect en actual, zodat de ontwikkelaar snel de bijbehorende implementatie kan vinden.
Interfaces met een platformfabriek — het belangrijkste alternatief voor expect/actual. In plaats van een expect-klasse kan een interface in commonMain worden gedeclareerd en concrete klassen in platformmodules worden gemaakt. De fabriek of dependency injection-container levert de juiste implementatie in runtime. Deze benadering is beter geschikt voor testen, omdat de interface kan worden gemockt.
Dependency Injection (Koin, Kodein) — een flexibelere maar minder performante benadering. De DI-container wordt afzonderlijk voor elk platform geconfigureerd en levert platformafhankelijkheden aan de gemeenschappelijke code. In tegenstelling tot expect/actual vindt injectie plaats in runtime, wat het mogelijk maakt om implementaties voor testen te vervangen. Aan de andere kant worden DI-configuratiefouten pas bij het opstarten ontdekt, niet in de compilatiefase.
| Benadering | Controle in compilatiefase | Testflexibiliteit | Runtime-overhead |
|---|---|---|---|
| expect/actual | Volledig | Laag (actual kan niet worden gemockt) | Geen (compilatiebinding) |
| Interfaces + fabriek | Gedeeltelijk | Hoog (kan worden gemockt) | Minimaal (virtuele aanroep) |
| Dependency Injection | Nee (runtime) | Hoog | Gemiddeld (DI-proxy) |
De keuze tussen expect/actual en alternatieven hangt af van de context. Voor kritieke prestaties (spelmotoren, real-time verwerking) heeft expect/actual de voorkeur vanwege nul runtime-overhead. Voor bedrijfslogica (repositories, use-cases) kun je beter interfaces met DI gebruiken om het testen te vereenvoudigen. Een gecombineerde benadering — expect/actual voor laag-niveau platformbewerkingen en interfaces voor de bedrijfslogicalaag — wordt in de meeste productie-KMM-projecten toegepast.
Veelgestelde vragen
expect/actual verbindt de implementatie in de compilatiefase zonder virtuele aanroepen, interfaces — in runtime. expect/actual garandeert de aanwezigheid van implementatie voor alle platforms, interfaces vereisen runtime-controles.
Ja, expect enum wordt ondersteund vanaf Kotlin 1.7. Alle constanten in expect en actual enum moeten overeenkomen. Verschillende waarden van constanten op verschillende platforms — compilatiefout.
De compiler geeft een foutmelding voor elk platform waar de actual-implementatie ontbreekt. Het project wordt niet gebouwd totdat voor alle expect-declaraties de bijbehorende actual-implementaties zijn toegevoegd.
Nee, expect en actual moeten in verschillende source sets staan. expect — in commonMain of een intermediaire source set, actual — in de platform source set. Het plaatsen van expect en actual in dezelfde source set is een compilatiefout.
Gebruik voor het testen van expect/actual commonTest met platform test source sets. Schrijf expect-tests in commonTest en actual-tests voor elk platform. Integratietests worden afzonderlijk op elk doelplatform uitgevoerd.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook