expect/actual — a Kotlin Multiplatform mechanizmusa, amely lehetővé teszi platformfüggő API-k deklarálását a közös kódban. Az expect kulcsszó egy függvény, osztály vagy tulajdonság szerződését hozza létre a commonMain-ben, az actual kulcsszó pedig konkrét implementációt biztosít minden platformhoz. A fordító ellenőrzi, hogy minden expect deklarációhoz tartozik-e actual implementáció az összes célplatformon. A JetBrains, 2025 adatai szerint a mechanizmust a KMM-projektek 80%-ában használják platform-specifikus üzleti logika megvalósítására.
Főbb pontok
expect/actual — a Kotlin Multiplatform deklaratív mechanizmusa platform-orientált programozás megvalósításához. Lehetővé teszi az API egyszeri leírását a közös modulban (expect) és különálló megvalósítását minden platformhoz (actual). Az interfészekkel ellentétben az expect/actual nem hoz létre virtuális hívásokat — a fordító összeköti az expect és actual deklarációkat a fordítási fázisban, ami kiküszöböli a dinamikus kiosztás többletterhelését.
Az expect/actual története a Kotlin Multiplatform 2017-es megjelenésével kezdődött. Kezdetben a mechanizmus expect/actual declarations néven volt ismert és kísérleti jellegű volt. A Kotlin 1.2-ben hozzáadták az expect annotációkat, a Kotlin 1.3-ban pedig az expect/actual stabil lett osztályok és függvények számára. Fokozatosan bővült a mechanizmus: a Kotlin 1.6-ban hozzáadták a companion objektumok támogatását, a Kotlin 1.7-ben — az enum osztályokét, a Kotlin 2.0-ban pedig — a typealias-okét.
Az expect/actual legfőbb jellemzője — biztonság fordítói szinten. Ha egy fejlesztő hozzáadott egy expect deklarációt a commonMain-hez, de elfelejtett actual implementációt biztosítani iOS-re, a fordító hibát jelez. Ez megakadályozza azokat a runtime hibákat, amelyek a reflexiós vagy dinamikus platformkód-betöltési megközelítésekre jellemzők.
A mechanizmus expect/actual source set szinten működik — a Kotlin Multiplatform modulrendszerében. Az összes platform számára elérhető közös kód a commonMain source set-ben található. A platformfüggő kód — az iosMain, androidMain, macosMain és így tovább. Az expect kulcsszó a commonMain-ben deklarálja az API-t, az actual kulcsszó a platform source set-ben pedig biztosítja az implementációt. A fordító összeköti őket a kódgenerálási fázisban, lecserélve az expect függvény hívását a megfelelő actual implementációra a célplatform számára.
A source set hierarchia egy tipikus KMM projektben így néz ki: commonMain tartalmazza az expect deklarációkat, iosMain és androidMain tartalmazza az actual implementációkat. iOS-re fordításkor az iosMain-ból származó actual, Androidra fordításkor pedig az androidMain-ból származó actual használatos. A source set-ek köztesek lehetnek (például iosArm64Main egy adott architektúrához), ami lehetővé teszi az implementációk finomítását különböző eszközökhöz.
// commonMain — expect deklaráció
expect fun getPlatformName(): String
// androidMain — actual Androidhoz
actual fun getPlatformName(): String = "Android"
// iosMain — actual iOS-hez
actual fun getPlatformName(): String = "iOS"
A Kotlin fordító több feltételt ellenőriz az expect/actual használatakor. Minden expect deklarációhoz tartoznia kell actual implementációnak minden aktív platformhoz. Az actual deklaráció aláírásának meg kell egyeznie az expect aláírásával (az @OptionalExpectation annotáció enyhítheti ezt a követelményt). A hozzáférési módosítóknak, visszatérési típusnak és paramétereknek azonosnak kell lenniük. A fordítð ellenőrzi továbbá a ciklikus függőségek hiányát az expect és actual deklarációk között.
expect/actual több deklarációtípust támogat. A leggyakrabban használtak az expect/actual függvények platform műveletekhez, az expect/actual osztályok natív implementációt igénylő objektumokhoz, és az expect/actual tulajdonságok konstanésokhoz és beállításokhoz. Minden típusnak megvannak a saját használati szabályai és korlátai.
Expect/actual függvények — a legegyszerűbb és legelterjedtebb típus. Platform API-k hívására használják, mint az idő lekérése, fájlok olvasása vagy HTTP kérések küldése. Expect/actual osztályok olyan objektumok létrehozására szolgálnak, amelyek közvetlenül érintkeznek natív kóddal (például kamera, geolokáció vagy kulcstároló eléréséhez). Expect/actual tulajdonságok (val) platform konstanésokhoz alkalmasak — operációs rendszer neve, SDK verzió vagy rendszerkönyvtár elérési útvonala.
| Deklaráció típusa | Kulcsszavak | Használati példa |
|---|---|---|
| Függvény | expect fun / actual fun | Eszköz egyedi azonosítójának lekérése |
| Osztály | expect class / actual class | Hozzáférés a SecureStorage-hoz (Keychain / EncryptedSharedPreferences) |
| Tulajdonság | expect val / actual val | Aktuális platform (iOS / Android) |
| Enum osztály | expect enum / actual enum | Elérhető alkalmazásengedélyek listája |
| Typealias | expect typealias / actual typealias | Platform-specifikus hálózati válasz típus |
Nem minden Kotlin konstrukció használható az expect/actual-lal. Az expect deklaráció nem tartalmazhat törzset — csak aláírást. Az expect osztály nem rendelkezhet paraméteres konstruktorral (üres elsődleges konstruktorral kell rendelkeznie). Az enum expect/actual esetén minden konstanésnak azonosnak kell lennie az expect-ben és actual-ban. Az expect tulajdonságoknak val-nak kell lenniük (nem var-nak), mivel az állapot tárolása a közös modulban platform tulajdonságok esetén értelmetlen.
Nézzük meg az expect/actual gyakorlati példáit egyszerű függvényektől a teljes osztályokig. Az alap eset — a platform név lekérése a felhasználói felületen való használathoz. Összetettebb példák tartalmazzák a natív tárolóhoz való hozzáférést és a platform szálakkal való munkát.
// commonMain — expect osztály biztonságos tároláshoz
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual Androidon
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() }
}
Ebben a példában az PlatformStorage expect osztály egy egyszerű kulcs-érték tároló szerződését határozza meg. Androidon az implementáció SharedPreferences-t használ, iOS-en pedig Keychain-t vagy NSUserDefaults-ot. Az expect/actual-nak köszönhetően a commonMain üzleti logikája meghívja a save/get/remove-ot anélkül, hogy ismerné a platform implementációt.
// iosMain — actual iOS-en Keychain-nel
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)
}
}
Az expect/actual API tervezésekor több elvet kell követni. Minimalizálja az expect deklarációk számát — minél több a közös kód, annál könnyebb a karbantartás. Csak azokhoz az API-khoz használja az expect/actual-t, amelyek ténylegesen különböznek a platformokon. A többi kódhoz alkalmazzon interfészeket gyárakkal vagy függőséginjektálással, ami leegyszerűsíti a tesztelést.
Ajánlott az expect deklarációkat tematikus modulokba csoportosítani, ahelyett hogy egy fájlban keverné őket. Például Storage.kt a tárolás expect deklarációihoz, Platform.kt az operációs rendszerrel való munka expect függvényeihez és Analytics.kt az analitika expect osztályaihoz. Ez leegyszerűsíti a navigációt és a KMM projekt platformfelületének megértését. Minden actual fájlnak a megfelelő source set-ben kell lennie: androidMain, iosMain, desktopMain és így tovább.
Alapértelmezett implementációk expect fun révén actual fun-nal, ahol az actual közös kódot használ — ez egy gyakori antipattern. Ha a platform implementáció nem különbözik az alapértelmezettől, nincs szükség expect/actual-ra. Ilyen esetekben használjon egyszerű függvényt a commonMain-ben. Kerülje továbbá az expect/actual-t triviális getterekhez — használjon expect val-t konstanésokkal.
Az expect/actual kód helyes struktúrája kritikus a projekt olvashatósága szempontjából. Minden expect/actual modulnak egyetlen belépési ponttal kell rendelkeznie. Példa a szervezésre: commonMain/kotlin/com/project/platform tartalmazza az expect deklarációkat, androidMain/kotlin/com/project/platform — actual Androidhoz, iosMain/kotlin/com/project/platform — actual iOS-hez. A fájl- és csomagneveknek meg kell egyezniük az expect és actual esetében, hogy a fejlesztő gyorsan megtalálja a megfelelő implementációt.
Interfészek platform gyárral — az expect/actual fő alternatívája. Az expect osztály helyett deklarálható egy interfész a commonMain-ben, és konkret osztályok hozhatók létre a platform modulokban. A gyár vagy függőséginjektáló konténer biztosítja a megfelelő implementációt runtime-ban. Ez a megközelítés jobb tesztelésre, mivel az interfész mockolható.
Függőséginjektálás (Koin, Kodein) — rugalmasabb, de kevésbé teljesítményes megközelítés. A DI konténer minden platformhoz külön van konfigurálva, és platform függőségeket biztosít a közös kódhoz. Az expect/actual-lal ellentétben az injektálás runtime-ban történik, ami lehetővé teszi az implementációk cseréjét tesztelés céljából. Másrészt a DI konfigurációs hibák csak indításkor derülnek ki, nem a fordítási fázisban.
| Megközelítés | Ellenőrzés fordítási fázisban | Tesztelési rugalmasság | Runtime többletterhelés |
|---|---|---|---|
| expect/actual | Teljes | Alacsony (actual nem mockolható) | Nulla (fordítási kötés) |
| Interfészek + gyár | Részleges | Magas (mockolható) | Minimális (virtuális hívás) |
| Függőséginjektálás | Nem (runtime) | Magas | Közepes (DI proxy) |
Az expect/actual és alternatívái közötti választás a kontextustól függ. Kritikus teljesítményhez (játékmotorok, valós idejű feldolgozás) az expect/actual előnyösebb a nulla runtime többletterhelés miatt. Üzleti logikához (repository-k, use-case-ek) jobb interfészeket használni DI-vel a tesztelés egyszerűsítése érdekében. A kombinált megközelítés — expect/actual alacsony szintű platform műveletekhez és interfészek az üzleti logikai réteghez — a legtöbb production KMM projektben alkalmazott.
Gyakran ismételt kérdések
expect/actual összeköti az implementációt a fordítási fázisban virtuális hívások nélkül, az interfészek pedig runtime-ban. Az expect/actual garantálja az implementáció meglétét minden platformhoz, az interfészek runtime ellenőrzéseket igényelnek.
Igen, az expect enum Kotlin 1.7 óta támogatott. Az expect és actual enum összes konstanésának meg kell egyeznie. Különböző értékek különböző platformokon — fordítási hiba.
A fordító hibát jelez minden olyan platformhoz, ahol hiányzik az actual implementáció. A projekt nem épül fel, amíg nem adnak hozzá megfelelő actual implementációkat az összes expect deklarációhoz.
Nem, expect és actual különböző source set-ekben kell lenniük. expect — a commonMain-ben vagy köztes source set-ben, actual — a platform source set-ben. Az expect és actual egy source set-be helyezése fordítási hiba.
Az expect/actual teszteléséhez használja a commonTest-et platform teszt source set-ekkel. Írjon expect teszteket a commonTest-ben és actual teszteket minden platformhoz. Az integrációs tesztek külön futnak minden célplatformon.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is