expect/actual — механизам Kotlin Multiplatform који омогућава декларисање АПИ-ја зависних од платформе у заједничком коду. Кључна реч expect ствара уговор функције, класе или особине у commonMain, а кључна реч actual пружа конкретну имплементацију за сваку платформу. Компајлер проверава да свакој expect декларацији одговара actual имплементација на свим циљним платформама. Према JetBrains, 2025, механизам се користи у 80% KMM пројеката за имплементацију пословне логике специфичне за платформу.
Главно
expect/actual — декларативни механизам Kotlin Multiplatform за имплементацију програмирања оријентисаног на платформу. Омогућава описивање АПИ-ја једном у заједничком модулу (expect) и његову имплементацију одвојено за сваку платформу (actual). За разлику од интерфејса, expect/actual не ствара виртуелне позиве — компајлер повезује expect и actual декларације у фази компилације, чиме се елиминишу додатни трошкови динамичке диспечерије.
Историја expect/actual почела је појавом Kotlin Multiplatform 2017. године. Првобитно се механизам звао expect/actual declarations и био је експерименталан. У Kotlin 1.2 додате су expect анотације, а у Kotlin 1.3 expect/actual је постао стабилан за класе и функције. Постепено се механизам ширио: у Kotlin 1.6 додата је подршка за companion објекте, у Kotlin 1.7 — за enum класе, а у Kotlin 2.0 — за typealias.
Кључна карактеристика expect/actual — безбедност на нивоу компајлера. Ако је програмер додао expect декларацију у commonMain, али заборавио да пружи actual имплементацију за iOS, компајлер ће пријавити грешку. Ово спречава runtime грешке карактеристичне за приступе са рефлексијом или динамичким учитавањем кода платформе.
Механизам expect/actual ради на нивоу source set — система модула Kotlin Multiplatform. Заједнички код доступан свим платформама налази се у commonMain source set-у. Код зависан од платформе — у iosMain, androidMain, macosMain и тако даље. Кључна реч expect у commonMain декларише АПИ, а кључна реч actual у source set-у платформе пружа имплементацију. Компајлер их повезује у фази генерисања кода, замењујући позив expect функције одговарајућом actual имплементацијом за циљну платформу.
Хијерархија source set-а у типичном KMM пројекту изгледа овако: commonMain садржи expect декларације, iosMain и androidMain садрже actual имплементације. При компилацији за iOS користи се actual из iosMain, при компилацији за Android — из androidMain. Source set-ови могу бити међупосредни (на пример, iosArm64Main за одређену архитектуру), што омогућава прецизирање имплементација за различите уређаје.
// commonMain — expect декларација
expect fun getPlatformName(): String
// androidMain — actual за Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual за iOS
actual fun getPlatformName(): String = "iOS"
Kotlin компајлер проверава неколико услова при раду са expect/actual. Свака expect декларација мора имати actual имплементацију за сваку активну платформу. Потпис actual декларације мора се поклапати са expect потписом (@OptionalExpectation анотација може ублажити овај захтев). Модификатори приступа, повратни тип и параметри морају бити идентични. Компајлер такође проверава одсуство цикличних зависности између expect и actual декларација.
expect/actual подржава неколико типова декларација. Најчешће се користе expect/actual функције за операције платформе, expect/actual класе за објекте који захтевају изворну имплементацију и expect/actual особине за константе и подешавања. Сваки тип има своја правила коришћења и ограничења.
Expect/actual функције — најједноставнији и најраспрострањенији тип. Користе се за позивање АПИ-ја платформе као што су добијање времена, читање датотека или слање HTTP захтева. Expect/actual класе примењују се за креирање објеката који директно интерагују са изворним кодом (на пример, за приступ камери, геолокацији или сигурном складишту). Expect/actual особине (val) погодне су за константе платформе — назив оперативног система, верзија SDK-а или путања до системског директоријума.
| Тип декларације | Кључне речи | Пример коришћења |
|---|---|---|
| Функција | expect fun / actual fun | Добијање јединственог идентификатора уређаја |
| Класа | expect class / actual class | Приступ SecureStorage (Keychain / EncryptedSharedPreferences) |
| Особина | expect val / actual val | Тренутна платформа (iOS / Android) |
| Enum класа | expect enum / actual enum | Листа доступних дозвола апликације |
| Typealias | expect typealias / actual typealias | Тип мрежног одговора специфичан за платформу |
Нису све Kotlin конструкције могуће користити са expect/actual. Expect декларација не може садржати тело — само потпис. Expect класа не може имати конструктор са параметрима (мора имати празан главни конструктор). За enum expect/actual све константе морају бити идентичне у expect и actual. Expect особине морају бити val (не var), јер чување стања у заједничком модулу за особине платформе нема смисла.
Размотримо практичне примере expect/actual од једноставних функција до потпуних класа. Основни случај — добијање назива платформе за коришћење у интерфејсу. Сложенији примери укључују приступ изворном складишту и рад са нитима платформе.
// commonMain — expect класа за сигурно складиште
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual на 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() }
}
У овом примеру, expect класа PlatformStorage дефинише уговор једноставног складишта кључ-вредност. На Android-у имплементација користи SharedPreferences, а на iOS-у — Keychain или NSUserDefaults. Захваљујући expect/actual, пословна логика у commonMain позива save/get/remove без познавања имплементације платформе.
// iosMain — actual на iOS-у са 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)
}
}
При пројектовању expect/actual АПИ-ја треба се придржавати неколико принципа. Минимизирајте број expect декларација — што је више заједничког кода, то је одржавање лакше. Користите expect/actual само за АПИ-је који се стварно разликују на платформама. За остали код примењујте интерфејсе са фабрикама или убризгавањем зависности, што поједностављује тестирање.
Препоручује се груписање expect декларација по тематским модулима, а не мешање у истом фајлу. На пример, Storage.kt за expect декларације складишта, Platform.kt за expect функције рада са оперативним системом и Analytics.kt за expect класе аналитике. Ово поједностављује навигацију и разумевање површине платформе KMM пројекта. Сваки actual фајл треба да се налази у одговарајућем source set-у: androidMain, iosMain, desktopMain и тако даље.
Подразумеване имплементације преко expect fun са actual fun, где actual користи заједнички код — распрострањени анти-образац. Ако се имплементација платформе не разликује од подразумеване, expect/actual није потребан. У таквим случајевима користите једноставну функцију у commonMain. Такође избегавајте expect/actual за тривијалне гетере — користите expect val са константама.
Правилна структура expect/actual кода је критична за читљивост пројекта. Сваки expect/actual модул треба да има јединствену улазну тачку. Пример организације: commonMain/kotlin/com/project/platform садржи expect декларације, androidMain/kotlin/com/project/platform — actual за Android, iosMain/kotlin/com/project/platform — actual за iOS. Називи фајлова и пакета треба да се поклапају за expect и actual, како би програмер могао брзо пронаћи одговарајућу имплементацију.
Интерфејси са фабриком платформе — главна алтернатива expect/actual. Уместо expect класе може се декларисати интерфејс у commonMain, а конкретне класе креирати у модулима платформе. Фабрика или контејнер за убризгавање зависности пружају исправну имплементацију у runtime-у. Овај приступ је бољи за тестирање јер се интерфејс може имитирати.
Убризгавање зависности (Koin, Kodein) — флексибилнији, али мање перформантан приступ. DI контејнер се конфигурише одвојено за сваку платформу и пружа зависности платформе заједничком коду. За разлику од expect/actual, убризгавање се дешава у runtime-у, што омогућава замену имплементација за тестирање. С друге стране, грешке конфигурације DI откривају се тек при покретању, а не у фази компилације.
| Приступ | Провера у фази компилације | Флексибилност тестирања | Runtime додатни трошкови |
|---|---|---|---|
| expect/actual | Потпуна | Ниска (actual се не може имитирати) | Нулти (везивање у компилацији) |
| Интерфејси + фабрика | Делимична | Висока (може се имитирати) | Минимални (виртуелни позив) |
| Убризгавање зависности | Не (runtime) | Висока | Средњи (DI прокси) |
Избор између expect/actual и алтернатива зависи од контекста. За критичне перформансе (погони игара, обрада у реалном времену) expect/actual је пожељнији због нултих runtime трошкова. За пословну логику (репозиторијуми, use-case-ови) боље је користити интерфејсе са DI како би се поједноставило тестирање. Комбиновани приступ — expect/actual за нискoнивoске операције платформе и интерфејси за слој пословне логике — примењује се у већини производних KMM пројеката.
Често постављана питања
expect/actual повезује имплементацију у фази компилације без виртуелних позива, а интерфејси — у runtime-у. expect/actual гарантује постојање имплементације за све платформе, интерфејси захтевају runtime провере.
Да, expect enum је подржан од Kotlin 1.7. Све константе у expect и actual enum морају се поклапати. Различите вредности константи на различитим платформама — грешка компилације.
Компајлер ће пријавити грешку за сваку платформу где недостаје actual имплементација. Пројекат се неће изградити док се за све expect декларације не додају одговарајуће actual имплементације.
Не, expect и actual морају бити у различитим source set-овима. expect — у commonMain или међупосредном source set-у, actual — у source set-у платформе. Постављање expect и actual у исти source set је грешка компилације.
За тестирање expect/actual користите commonTest са source set-овима за тестирање платформе. Напишите expect тестове у commonTest и actual тестове за сваку платформу. Интеграциони тестови се покрећу одвојено на свакој циљној платформи.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође