expect/actual — суть, ключові слова KMM та як вони працюють

Автор: IT Sectr Опубліковано: 2026-06-05 Час читання: 8 хв

expect/actual — це механізм Kotlin Multiplatform, який дозволяє оголошувати платформозалежні API в спільному коді. Ключове слово expect створює контракт функції, класу або властивості в commonMain, а ключове слово actual надає конкретну реалізацію для кожної платформи. Компілятор перевіряє, що кожному оголошенню expect відповідає реалізація actual на всіх цільових платформах. За даними JetBrains, 2025, цей механізм використовується в 80% KMM-проєктів для реалізації платформенної бізнес-логіки.

Головне

  • expect — ключове слово для оголошення контракту функції, класу або властивості в спільному коді.
  • actual — ключове слово для надання платформенної реалізації оголошення expect.
  • commonMain — source set зі спільним кодом, де розміщуються оголошення expect.
  • Перевірка компілятора — компілятор гарантує наявність реалізацій actual для всіх цільових платформ.
  • Source set — набори (iosMain, androidMain), де розміщуються платформенні реалізації actual.

Що таке expect/actual?

expect/actual — це декларативний механізм Kotlin Multiplatform для реалізації платформоорієнтованого програмування. Він дозволяє описати API одного разу в спільному модулі (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 додав підтримку expect/actual для companion-об’єктів, Kotlin 1.7 — для enum-класів, а Kotlin 2.0 — для typealias.

Ключова особливість expect/actual — це безпека на рівні компіляції. Якщо розробник додає оголошення expect в commonMain, але забуває надати реалізацію actual для iOS, компілятор створить помилку. Це запобігає помилкам часу виконання, характерним для підходів з рефлексією або динамічним завантаженням платформенного коду.

Як працює механізм expect/actual

Механізм expect/actual працює на рівні source set — системи модулів Kotlin Multiplatform. Спільний код, доступний для всіх платформ, знаходиться в source set commonMain. Платформозалежний код знаходиться в iosMain, androidMain, macosMain тощо. Ключове слово expect в commonMain оголошує API, а ключове слово actual в платформенному source set надає реалізацію. Компілятор пов’язує їх на етапі генерації коду, замінюючи виклик функції expect на відповідну реалізацію actual для цільової платформи.

Єрархія source set у типовому KMM-проєкті виглядає так: commonMain містить оголошення expect, iosMain та androidMain містять реалізації actual. При компіляції для iOS використовується actual з iosMain, при компіляції для Android — actual з androidMain. Source set можуть бути проміжними (наприклад, iosArm64Main для конкретної архітектури), що дозволяє уточнювати реалізації для різних пристроїв.

kotlin
// 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"

Компіляторна перевірка actual-реалізацій

Компілятор Kotlin перевіряє кілька умов при роботі з expect/actual. Кожне оголошення expect повинно мати actual-реалізацію для кожної активної платформи. Сигнатура actual-оголошення повинна збігатися з сигнатурою expect (анотація @OptionalExpectation може пом’якшувати цю вимогу). Модифікатори доступу, тип повернення та параметри повинні бути ідентичними. Компілятор також перевіряє відсутність циклічних залежностей між оголошеннями expect та actual.

Типи expect/actual: функції, класи, властивості

expect/actual підтримує кілька типів оголошень. Найчастіше використовуються функції expect/actual для платформенних операцій, класи expect/actual для об’єктів, що потребують нативної реалізації, та властивості expect/actual для констант та налаштувань. Кожен тип має свої правила використання та обмеження.

Функції expect/actual — найпростіший та найпоширеніший тип. Вони використовуються для виклику платформенних API, таких як отримання часу, читання файлів або надсилання HTTP-запитів. Класи expect/actual використовуються для створення об’єктів, які безпосередньо взаємодіють з нативним кодом (наприклад, для доступу до камери, геолокації або сховища ключів). Властивості expect/actual (val) підходять для платформенних констант — назва OS, версія SDK або шлях до системної директорії.

Тип оголошенняКлючові словаПриклад використання
Функціяexpect fun / actual funОтримання унікального ідентифікатора пристрою
Класexpect class / actual classДоступ до SecureStorage (Keychain / EncryptedSharedPreferences)
Властивістьexpect val / actual valПоточна платформа (iOS / Android)
Enum-класexpect enum / actual enumСписок доступних дозволів додатка
Typealiasexpect typealias / actual typealiasТип відповіді мережі, що залежить від платформи

Обмеження expect/actual

Не всі конструкції Kotlin можуть бути використані з expect/actual. Оголошення expect не може містити тіло — лише сигнатуру. Клас expect не може мати конструктор з параметрами (повиннен мати порожній первинний конструктор). Для enum expect/actual всі константи повинні бути однаковими в expect та actual. Властивості expect повинні бути val (не var), оскільки зберігання стану в спільному модулі для платформенних властивостей не має сенсу.

Приклади коду: від простого до складного

Розглянемо практичні приклади expect/actual від простих функцій до повноцінних класів. Базовий випадок — отримання назви платформи для використання в інтерфейсі користувача. Складніші приклади включають доступ до нативного сховища та роботу з платформенними потоками.

kotlin
// 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() }
}

У цьому прикладі клас expect PlatformStorage визначає контракт простого сховища ключ-значення. На Android реалізація використовує SharedPreferences, а на iOS — Keychain або NSUserDefaults. Завдяки expect/actual, бізнес-логіка в commonMain викликає save/get/remove, не знаючи про платформенну реалізацію.

kotlin
// 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)
    }
}

Кращі практики expect/actual

При проектуванні API expect/actual слід дотримуватися кількох принципів. Мінімізуйте кількість оголошень expect — чим більше спільного коду, тим простіше обслуговування. Використовуйте expect/actual лише для API, які дійсно відрізняються на різних платформах. Для іншого коду використовуйте інтерфейси з фабриками або ін’єкцією залежностей, що спрощує тестування.

Рекомендується групувати оголошення expect за тематичними модулями, а не змішувати їх в одному файлі. Наприклад, Storage.kt для оголошень expect щодо сховища, Platform.kt для функцій expect для роботи з OS та 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 в KMM

Інтерфейси з платформенною фабрикою — основна альтернатива expect/actual. Замість класу expect ви можете оголосити інтерфейс в commonMain та створити конкретні класи в платформенних модулях. Фабрика або контейнер ін’єкції залежностей надає правильну реалізацію під час виконання. Цей підхід краще підходить для тестування, оскільки інтерфейс можна замокати.

Ін’єкція залежностей (Koin, Kodein) — більш гнучкий, але менш ефективний підхід. Контейнер DI налаштовується окремо для кожної платформи та надає платформенні залежності спільному коду. На відміну від expect/actual, ін’єкція відбувається під час виконання, що дозволяє замінювати реалізації для тестування. З іншого боку, помилки конфігурації DI виявляються лише під час виконання, а не на етапі компіляції.

ПідхідПеревірка на компіляціїГнучкість тестуванняНакладні витрати на виконанні
expect/actualПовнаНизька (actual не можна замокати)Нульові (компіляційний зв’язок)
Інтерфейси + ФабрикаЧастковаВисока (можна замокати)Мінімальні (віртуальний виклик)
Ін’єкція залежностейНі (виконання)ВисокаСередні (DI-проксі)

Вибір між expect/actual та альтернативами залежить від контексту. Для критичного для продуктивності коду (ігрові рушії, обробка в реальному часі) expect/actual краще через нульові накладні витрати. Для бізнес-логіки (репозиторії, use-case) краще використовувати інтерфейси з DI, щоб спростити тестування. Комбінований підхід — expect/actual для низькорівневих платформенних операцій та інтерфейси для рівня бізнес-логіки — використовується в більшості виробничих KMM-проєктів.

Часті запитання

Чим відрізняється expect/actual від інтерфейсів?

expect/actual пов’язує реалізацію на етапі компіляції без віртуальних викликів, а інтерфейси — під час виконання. expect/actual гарантує наявність реалізації для всіх платформ, інтерфейси потребують перевірок під час виконання.

Чи можна використовувати expect/actual для enum?

Так, expect enum підтримується починаючи з Kotlin 1.7. Всі константи в expect та actual enum повинні збігатися. Різні значення констант на різних платформах — помилка компіляції.

Що буде, якщо забути actual-реалізацію?

Компілятор створить помилку для кожної платформи, де відсутня actual-реалізація. Проєкт не збереться, доки для всіх оголошень expect не будуть додані відповідні actual-реалізації.

Чи можна використовувати expect/actual всередині одного source set?

Ні, expect та actual повинні знаходитися в різних source setах. expect в commonMain або проміжному source set, actual в платформенному source set. Розміщення expect та actual в одному source set — помилка компіляції.

Як тестувати код expect/actual?

Для тестування expect/actual використовуйте commonTest з платформенними тестовими source setами. Напишіть тести expect в commonTest та тести actual для кожної платформи. Інтеграційні тести запускаються окремо на кожній цільовій платформі.

Підсумки

  • expect/actual — ключовий механізм Kotlin Multiplatform для платформенних реалізацій з компіляторною перевіркою.
  • expect оголошує контракт в commonMain, actual надає реалізацію в платформенному source set.
  • Типи оголошень включають функції, класи, властивості, enum-класи та typealias з різними правилами використання.
  • Компіляторна перевірка гарантує наявність actual-реалізацій для всіх цільових платформ, запобігаючи помилкам часу виконання.
  • Рекомендується мінімізувати expect/actual та використовувати інтерфейси з DI для бізнес-логіки.
  • Організація коду повинна бути єдинообразною з збігом імен файлів та пакетів для expect та actual.
  • Використовуйте expect/actual для низькорівневих платформенних операцій (сховище, файлова система, датчики) — це забезпечує нульові накладні витрати під час виконання.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також