expect/actual ist ein Kotlin-Multiplatform-Mechanismus, der die Deklaration plattformabhängiger APIs in gemeinsamem Code ermöglicht. Das Schlüsselwort expect erstellt einen Vertrag für eine Funktion, Klasse oder Eigenschaft in commonMain, während das Schlüsselwort actual eine konkrete Implementierung für jede Plattform bereitstellt. Der Compiler überprüft, dass jede expect-Deklaration eine entsprechende actual-Implementierung auf allen Zielplattformen hat. Laut JetBrains, 2025 wird dieser Mechanismus in 80% der KMM-Projekte zur Implementierung plattformübergreifender Geschäftslogik verwendet.
Wichtige Punkte
expect/actual ist ein deklarativer Mechanismus von Kotlin Multiplatform zur Implementierung plattformorientierter Programmierung. Er ermöglicht es, eine API einmal im gemeinsamen Modul (expect) zu beschreiben und sie getrennt für jede Plattform (actual) zu implementieren. Im Gegensatz zu Interfaces erzeugt expect/actual keine virtuellen Aufrufe — der Compiler verbindet expect- und actual-Deklarationen zur Compile-Zeit, wodurch der Overhead der dynamischen Dispatch entfällt.
Die Geschichte von expect/actual begann mit der Einführung von Kotlin Multiplatform im Jahr 2017. Ursprünglich hieß der Mechanismus expect/actual declarations und war experimentell. In Kotlin 1.2 wurden expect-Annotationen hinzugefügt, und in Kotlin 1.3 wurde expect/actual für Klassen und Funktionen stabil. Im Laufe der Zeit wurde der Mechanismus erweitert: Kotlin 1.6 fügte Unterstützung für expect/actual bei Companion-Objekten hinzu, Kotlin 1.7 für Enum-Klassen und Kotlin 2.0 für Typealias.
Das Hauptmerkmal von expect/actual ist die Compile-Zeit-Sicherheit. Wenn ein Entwickler eine expect-Deklaration in commonMain hinzufügt, aber vergisst, eine actual-Implementierung für iOS bereitzustellen, erzeugt der Compiler einen Fehler. Dies verhindert Laufzeitfehler, die bei Ansätzen mit Reflektion oder dynamischem Laden von Plattformcode häufig auftreten.
Der Mechanismus von expect/actual arbeitet auf Source-Set-Ebene — dem Modulsystem von Kotlin Multiplatform. Gemeinsamer Code, der allen Plattformen zur Verfügung steht, befindet sich im commonMain-Source-Set. Plattformabhängiger Code befindet sich in iosMain, androidMain, macosMain usw. Das Schlüsselwort expect in commonMain deklariert eine API, während das Schlüsselwort actual in einem Plattform-Source-Set die Implementierung bereitstellt. Der Compiler verbindet sie in der Codegenerierungsphase und ersetzt den expect-Funktionsaufruf durch die entsprechende actual-Implementierung für die Zielplattform.
Die Source-Set-Hierarchie in einem typischen KMM-Projekt sieht wie folgt aus: commonMain enthält expect-Deklarationen, iosMain und androidMain enthalten actual-Implementierungen. Beim Kompilieren für iOS wird das actual aus iosMain verwendet, beim Kompilieren für Android das actual aus androidMain. Source Sets können intermediär sein (z. B. iosArm64Main für eine bestimmte Architektur), was die Verfeinerung von Implementierungen für verschiedene Geräte ermöglicht.
// commonMain — expect declaration
expect fun getPlatformName(): String
// androidMain — actual for Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual for iOS
actual fun getPlatformName(): String = "iOS"
Der Kotlin-Compiler prüft mehrere Bedingungen bei der Arbeit mit expect/actual. Jede expect-Deklaration muss eine actual-Implementierung für jede aktive Plattform haben. Die Signatur der actual-Deklaration muss mit der expect-Signatur übereinstimmen (die Annotation @OptionalExpectation kann diese Anforderung lockern). Zugriffsmodifikatoren, Rückgabetyp und Parameter müssen identisch sein. Der Compiler prüft auch das Fehlen zyklischer Abhängigkeiten zwischen expect- und actual-Deklarationen.
expect/actual unterstützt mehrere Arten von Deklarationen. Am häufigsten verwendet werden expect/actual-Funktionen für Plattformoperationen, expect/actual-Klassen für Objekte, die native Implementierung benötigen, und expect/actual-Eigenschaften für Konstanten und Einstellungen. Jede Art hat ihre eigenen Nutzungsregeln und Einschränkungen.
Expect/actual-Funktionen sind der einfachste und gebräuchlichste Typ. Sie werden verwendet, um Plattform-APIs wie Zeitabfrage, Dateilesen oder HTTP-Anfragen aufzurufen. Expect/actual-Klassen werden verwendet, um Objekte zu erstellen, die direkt mit nativem Code interagieren (z. B. für den Zugriff auf Kamera, Geolokalisierung oder Schlüsselspeicher). Expect/actual-Eigenschaften (val) eignen sich für Plattformkonstanten — OS-Name, SDK-Version oder Systemverzeichnispfad.
| Deklarationstyp | Schlüsselwörter | Anwendungsbeispiel |
|---|---|---|
| Funktion | expect fun / actual fun | Eindeutige Gerätekennung abrufen |
| Klasse | expect class / actual class | Auf SecureStorage zugreifen (Keychain / EncryptedSharedPreferences) |
| Eigenschaft | expect val / actual val | Aktuelle Plattform (iOS / Android) |
| Enum-Klasse | expect enum / actual enum | Liste verfügbarer App-Berechtigungen |
| Typealias | expect typealias / actual typealias | Plattformspezifischer Netzwerkantworttyp |
Nicht alle Kotlin-Konstrukte können mit expect/actual verwendet werden. Eine expect-Deklaration darf keinen Rumpf enthalten — nur eine Signatur. Eine expect-Klasse darf keinen Konstruktor mit Parametern haben (sie muss einen leeren primären Konstruktor haben). Für enum expect/actual müssen alle Konstanten in expect und actual identisch sein. Expect-Eigenschaften müssen val (nicht var) sein, da das Speichern von Zustand im gemeinsamen Modul für Plattformeigenschaften keinen Sinn ergibt.
Lassen Sie uns praktische Beispiele für expect/actual von einfachen Funktionen bis zu vollständigen Klassen erkunden. Der Basisfall ist das Abrufen des Plattformnamens für die Verwendung in der Benutzeroberfläche. Komplexere Beispiele umfassen den Zugriff auf nativen Speicher und die Arbeit mit Plattform-Threads.
// commonMain — expect class for secure storage
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual on 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 diesem Beispiel definiert die expect-Klasse PlatformStorage den Vertrag eines einfachen Schlüssel-Wert-Speichers. Unter Android verwendet die Implementierung SharedPreferences, unter iOS Keychain oder NSUserDefaults. Dank expect/actual ruft die Geschäftslogik in commonMain save/get/remove auf, ohne die Plattformimplementierung zu kennen.
// iosMain — actual on iOS with 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)
}
}
Beim Entwerfen von expect/actual-APIs sollten mehrere Prinzipien befolgt werden. Minimieren Sie die Anzahl der expect-Deklarationen — je mehr gemeinsamer Code, desto einfacher die Wartung. Verwenden Sie expect/actual nur für APIs, die sich zwischen Plattformen tatsächlich unterscheiden. Für den restlichen Code verwenden Sie Interfaces mit Fabriken oder Dependency Injection, was das Testen vereinfacht.
Es wird empfohlen, expect-Deklarationen nach thematischen Modulen zu gruppieren, anstatt sie in einer Datei zu vermischen. Zum Beispiel Storage.kt für expect-Deklarationen zum Speicher, Platform.kt für expect-Funktionen zur Arbeit mit dem Betriebssystem und Analytics.kt für expect-Analyseklassen. Dies vereinfacht die Navigation und das Verständnis der Plattformoberfläche eines KMM-Projekts. Jede actual-Datei sollte im entsprechenden Source Set liegen: androidMain, iosMain, desktopMain usw.
Standardimplementierungen über expect fun mit actual fun, wobei actual gemeinsamen Code verwendet, ist ein häufiges Antipattern. Wenn sich die Plattformimplementierung nicht vom Standard unterscheidet, ist expect/actual nicht erforderlich. Verwenden Sie in solchen Fällen eine einfache Funktion in commonMain. Vermeiden Sie auch expect/actual für triviale Getter — verwenden Sie expect val mit Konstanten.
Die richtige Struktur von expect/actual-Code ist entscheidend für die Lesbarkeit des Projekts. Jedes expect/actual-Modul sollte einen einzigen Einstiegspunkt haben. Beispielorganisation: commonMain/kotlin/com/project/platform enthält expect-Deklarationen, androidMain/kotlin/com/project/platform enthält actual für Android, iosMain/kotlin/com/project/platform enthält actual für iOS. Datei- und Paketnamen müssen für expect und actual übereinstimmen, damit ein Entwickler schnell die entsprechende Implementierung finden kann.
Interfaces mit einer Plattformfabrik sind die Hauptalternative zu expect/actual. Anstelle einer expect-Klasse können Sie ein Interface in commonMain deklarieren und konkrete Klassen in den Plattformmodulen erstellen. Eine Fabrik oder ein Dependency-Injection-Container stellt die korrekte Implementierung zur Laufzeit bereit. Dieser Ansatz eignet sich besser zum Testen, da das Interface mockbar ist.
Dependency Injection (Koin, Kodein) ist ein flexiblerer, aber weniger leistungsfähiger Ansatz. Ein DI-Container wird getrennt für jede Plattform konfiguriert und stellt dem gemeinsamen Code Plattformabhängigkeiten bereit. Im Gegensatz zu expect/actual erfolgt die Injektion zur Laufzeit, was das Austauschen von Implementierungen zu Testzwecken ermöglicht. Andererseits werden DI-Konfigurationsfehler nur zur Laufzeit erkannt, nicht zur Compile-Zeit.
| Ansatz | Compile-Zeit-Prüfung | Testflexibilität | Laufzeit-Overhead |
|---|---|---|---|
| expect/actual | Vollständig | Niedrig (actual kann nicht gemockt werden) | Null (Compile-Zeit-Bindung) |
| Interfaces + Fabrik | Teilweise | Hoch (kann gemockt werden) | Minimal (virtueller Aufruf) |
| Dependency Injection | Nein (Laufzeit) | Hoch | Mittel (DI-Proxies) |
Die Wahl zwischen expect/actual und Alternativen hängt vom Kontext ab. Für leistungskritischen Code (Spiele-Engines, Echtzeitverarbeitung) ist expect/actual aufgrund des fehlenden Overheads vorzuziehen. Für Geschäftslogik (Repositories, Use Cases) ist es besser, Interfaces mit DI zu verwenden, um das Testen zu vereinfachen. Ein kombinierter Ansatz — expect/actual für niedrige Plattformoperationen und Interfaces für die Geschäftslogikschicht — wird in den meisten Production-KMM-Projekten verwendet.
Häufig gestellte Fragen
expect/actual bindet die Implementierung zur Compile-Zeit ohne virtuelle Aufrufe, während Interfaces zur Laufzeit binden. expect/actual garantiert Implementierung für alle Plattformen, Interfaces erfordern Laufzeitprüfungen.
Ja, expect enum wird ab Kotlin 1.7 unterstützt. Alle Konstanten in expect- und actual-Enums müssen übereinstimmen. Unterschiedliche Konstantenwerte auf verschiedenen Plattformen sind ein Compile-Fehler.
Der Compiler erzeugt einen Fehler für jede Plattform, auf der die actual-Implementierung fehlt. Das Projekt wird nicht gebaut, bis für alle expect-Deklarationen entsprechende actual-Implementierungen hinzugefügt wurden.
Nein, expect und actual müssen in verschiedenen Source Sets sein. expect in commonMain oder einem intermediären Source Set, actual in einem Plattform-Source Set. Das Platzieren von expect und actual im selben Source Set ist ein Compile-Fehler.
Zum Testen von expect/actual verwenden Sie commonTest mit Plattform-Test-Source-Sets. Schreiben Sie expect-Tests in commonTest und actual-Tests für jede Plattform. Integrationstests werden separat auf jeder Zielplattform ausgeführt.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch