expect/actual — mechanismus Kotlin Multiplatform, který umožňuje deklarovat platformově závislá API ve společném kódu. Klíčové slovo expect vytváří kontrakt funkce, třídy nebo vlastnosti v commonMain, zatímco klíčové slovo actual poskytuje konkrétní implementaci pro každou platformu. Kompilátor kontroluje, že každé expect-deklaraci odpovídá actual-implementace na všech cílových platformách. Podle údajů JetBrains, 2025 se mechanismus používá v 80 % KMM-projektů pro implementaci platformové obchodní logiky.
Hlavní body
expect/actual — je deklarativní mechanismus Kotlin Multiplatform pro implementaci platformově orientovaného programování. Umožňuje popsat API jednou ve společném modulu (expect) a implementovat jej zvlášť pro každou platformu (actual). Na rozdíl od rozhraní expect/actual nevytváří virtuální volání — kompilátor spojuje expect- a actual-deklarace ve fázi kompilace, což vylučuje režii dynamického směrování.
Historie expect/actual začala s příchodem Kotlin Multiplatform v roce 2017. Původně se mechanismus nazýval expect/actual declarations a byl experimentální. V Kotlin 1.2 byly přidány expect-anotace a v Kotlin 1.3 se expect/actual stal stabilním pro třídy a funkce. Postupně se mechanismus rozšiřoval: v Kotlin 1.6 byla přidána podpora expect/actual pro companion-objekty, v Kotlin 1.7 — pro enum-třídy a v Kotlin 2.0 — pro typealias.
Klíčovou vlastností expect/actual je bezpečnost na úrovni kompilace. Pokud vývojář přidá expect-deklaraci do commonMain, ale zapomene poskytnout actual-implementaci pro iOS, kompilátor vydá chybu. To zabraňuje runtime-chybám typickým pro přístupy s reflexí nebo dynamickým načítáním platformového kódu.
Mechanismus expect/actual pracuje na úrovni source set — systém modulů Kotlin Multiplatform. Společný kód přístupný všem platformám je umístěn v source setu commonMain. Platformově závislý kód — v iosMain, androidMain, macosMain atd. Klíčové slovo expect v commonMain deklaruje API, zatímco klíčové slovo actual v platformovém source setu poskytuje implementaci. Kompilátor je spojuje ve fázi generování kódu, nahrazuje volání expect-funkce odpovídající actual-implementací pro cílovou platformu.
Hierarchie source set v typickém KMM-projektu vypadá následovně: commonMain obsahuje expect-deklarace, iosMain a androidMain obsahují actual-implementace. Při kompilaci pro iOS se používá actual z iosMain, při kompilaci pro Android — z androidMain. Source sety mohou být střední (např. iosArm64Main pro konkrétní architekturu), což umožňuje upřesnit implementace pro různá zařízení.
// commonMain — deklarace expect
expect fun getPlatformName(): String
// androidMain — actual pro Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual pro iOS
actual fun getPlatformName(): String = "iOS"
Kompilátor Kotlin kontroluje několik podmínek při práci s expect/actual. Každá expect-deklarace musí mít actual-implementaci pro každou aktivní platformu. Signatura actual-deklarace musí odpovídat expect-signatuře (anotace @OptionalExpectation může toto požadavky zmírnit). Modifikátory přístupu, návratový typ a parametry musí být identické. Kompilátor také kontroluje nepřítomnost cyklických závislostí mezi expect- a actual-deklaracemi.
expect/actual podporuje několik typů deklarací. Nejčastěji se používají expect/actual-funkce pro platformové operace, expect/actual-třídy pro objekty vyžadující nativní implementaci a expect/actual-vlastnosti pro konstanty a nastavení. Každý typ má svá pravidla použití a omezení.
Expect/actual-funkce — nejjednodušší a nejrozšířenější typ. Používají se pro volání platformových API, jako je získání času, čtení souborů nebo odesílání HTTP požadavků. Expect/actual-třídy se používají pro vytváření objektů, které přímo interagují s nativním kódem (např. pro přístup ke kameře, geolokaci nebo bezpečnému úložišti). Expect/actual-vlastnosti (val) jsou vhodné pro platformové konstanty — název OS, verzi SDK nebo cestu k systémovému adresáři.
| Typ deklarace | Klíčová slova | Příklad použití |
|---|---|---|
| Funkce | expect fun / actual fun | Získání unikátního identifikátoru zařízení |
| Třída | expect class / actual class | Přístup k SecureStorage (Keychain / EncryptedSharedPreferences) |
| Vlastnost | expect val / actual val | Aktuální platforma (iOS / Android) |
| Enum-třída | expect enum / actual enum | Seznam dostupných oprávnění aplikace |
| Typealias | expect typealias / actual typealias | Typ síťové odpovědi specifický pro platformu |
Ne všechny konstrukce Kotlin lze použít s expect/actual. Expect-deklarace nemůže obsahovat tělo — pouze signaturu. Expect-třída nemůže mít konstruktor s parametry (musí mít prázdný primary constructor). Pro enum expect/actual musí být všechny konstanty stejné v expect i actual. Expect-vlastnosti musí být val (ne var), protože ukládání stavu ve společném modulu pro platformové vlastnosti nedává smysl.
Podívejme se na praktické příklady expect/actual od jednoduchých funkcí až po plnohodnotné třídy. Základní případ — získání názvu platformy pro použití v UI. Složitější příklady zahrnují přístup k nativnímu úložišti a práci s platformovými vlákny.
// commonMain — expect třída pro bezpečné úložiště
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual na Androidu
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() }
}
V tomto příkladu expect-třída PlatformStorage definuje kontrakt jednoduchého úložiště klíč-hodnota. Na Androidu implementace používá SharedPreferences, na iOS — Keychain nebo NSUserDefaults. Díky expect/actual volá obchodní logika v commonMain save/get/remove, aniž by věděla o platformové implementaci.
// iosMain — actual na iOS s 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)
}
}
Při navrhování expect/actual API byste se měli řídit několika zásadami. Minimalizujte počet expect-deklarací — čím více společného kódu, tím snazší údržba. Používejte expect/actual pouze pro ta API, která se na platformách skutečně liší. Pro zbytek kódu používejte rozhraní s továrnami nebo dependency injection, což zjednodušuje testování.
Doporučuje se seskupovat expect-deklarace do tematických modulů, nikoli je míchat v jednom souboru. Například Storage.kt pro expect-deklarace úložiště, Platform.kt pro expect-funkce práce s OS a Analytics.kt pro expect-třídy analýzy. To zjednodušuje navigaci a pochopení platformového povrchu KMM-projektu. Každý actual-soubor by měl být v odpovídajícím source setu: androidMain, iosMain, desktopMain atd.
Výchozí implementace pomocí expect fun s actual fun, kde actual používá společný kód, je běžný anti-vzor. Pokud se platformová implementace neliší od výchozí, expect/actual není potřeba. V takových případech použijte jednoduchou funkci v commonMain. Také se vyhněte expect/actual pro triviální gettery — použijte expect val s konstantami.
Správná struktura expect/actual kódu je kritická pro čitelnost projektu. Každý expect/actual modul by měl mít jediný vstupní bod. Příklad organizace: commonMain/kotlin/com/project/platform obsahuje expect-deklarace, androidMain/kotlin/com/project/platform — actual pro Android, iosMain/kotlin/com/project/platform — actual pro iOS. Názvy souborů a balíčků by měly odpovídat pro expect i actual, aby vývojář rychle našel odpovídající implementaci.
Rozhraní s platformovou továrnou — hlavní alternativa k expect/actual. Místo expect-třídy můžete deklarovat rozhraní v commonMain a vytvořit konkrétní třídy v platformových modulech. Továrna nebo dependency injection kontejner poskytuje správnou implementaci za běhu. Tento přístup je vhodnější pro testování, protože rozhraní lze mockovat.
Dependency Injection (Koin, Kodein) — flexibilnější, ale méně výkonný přístup. DI-kontejner se nastavuje zvlášť pro každou platformu a poskytuje platformové závislosti do společného kódu. Na rozdíl od expect/actual probíhá injekce za běhu, což umožňuje nahrazovat implementace pro testování. Nevýhodou je, že chyby konfigurace DI se odhalí až při spuštění, nikoli při kompilaci.
| Přístup | Kontrola při kompilaci | Flexibilita testování | Režie za běhu |
|---|---|---|---|
| expect/actual | Úplná | Nízká (actual nelze mockovat) | Žádná (kompilační vazba) |
| Rozhraní + továrna | Částečná | Vysoká (lze mockovat) | Minimální (virtuální volání) |
| Dependency Injection | Ne (runtime) | Vysoká | Střední (DI-proxy) |
Volba mezi expect/actual a alternativami závisí na kontextu. Pro kritický výkon (herní enginy, zpracování v reálném čase) je expect/actual výhodnější díky nulové režii. Pro obchodní logiku (repozitáře, use-cases) je lepší použít rozhraní s DI pro zjednodušení testování. Kombinovaný přístup — expect/actual pro nízkoúrovňové platformové operace a rozhraní pro vrstvu obchodní logiky — se používá ve většině produkčních KMM-projektů.
Často kladené otázky
expect/actual spojuje implementaci ve fázi kompilace bez virtuálních volání, zatímco rozhraní — za běhu. expect/actual garantuje existenci implementace pro všechny platformy, rozhraní vyžadují runtime-kontroly.
Ano, expect enum je podporován od Kotlin 1.7. Všechny konstanty v expect a actual enum musí být shodné. Různé hodnoty konstant na různých platformách jsou chybou kompilace.
Kompilátor vydá chybu pro každou platformu, kde actual-implementace chybí. Projekt se nesestaví, dokud nebudou pro všechny expect-deklarace přidány odpovídající actual-implementace.
Ne, expect a actual musí být v různých source setech. expect — v commonMain nebo středním source setu, actual — v platformovém source setu. Umístění expect a actual ve stejném source setu je chybou kompilace.
Pro testování expect/actual použijte commonTest s platformovými testovacími source sety. Napište expect-testy v commonTest a actual-testy pro každou platformu. Integrační testy se spouštějí zvlášť na každé cílové platformě.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také