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, компилятор выдаст ошибку. Это предотвращает runtime-сбои, характерные для подходов с рефлексией или динамической загрузкой платформенного кода.

Как работи механизъмът expect/actual

Механизм expect/actual работает на уровне source set — системы модулей Kotlin Multiplatform. Общий код, доступный всем платформам, располагается в source set'е commonMain. Платформенно-зависимый код — в iosMain, androidMain, macosMain и так далее. Ключевое слово expect в commonMain объявляет API, а keyчевое слово actual в платформенном source set'е предоставляет реализацию. Компилятор связывает их на этапе кодогенерации, заменяя вызов expect-функции на соответствующую actual-реализацию для целевой платформы.

Source set иерархия в типичном KMM-проекте выглядит следующим образом: commonMain содержит expect-декларации, iosMain и androidMain содержат actual-реализации. При компиляции для iOS используется actual из iosMain, при компиляции для Android — из 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 может смягчать это требование). Модификаторы доступа, return type и параметры должны быть идентичны. Компилятор также проверяет отсутствие циклических зависимостей между expect- и actual-декларациями.

Видове expect/actual: функции, класове, свойства

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

Expect/actual-функции — самый простой и распространённый тип. Они используются для вызова платформенных API, таких как получение времени, чтение файлов или отправка 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Список доступных разрешений приложения
Typealiasexpect typealias / actual typealiasТип сетевого ответа, специфичный для платформы

Ограничения expect/actual

Не все конструкции Kotlin могут быть использованы с expect/actual. Expect-декларация не может содержать тело — только сигнатуру. Expect-класс не может иметь конструктор с параметрами (должен быть пустой primary constructor). Для enum expect/actual все константы должны быть одинаковыми в expect и actual. Expect-свойства должны быть val (не var), так как хранение состояния в общем модуле для платформенных свойств не имеет смысла.

Примери на код: от просто към сложно

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

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

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

Рекомендуется группировать expect-декларации по тематическим модулям, а не смешивать их в одном файле. Например, Storage.kt для expect-деклараций хранилища, Platform.kt для expect-функций работы с ОС и Analytics.kt для expect-классов аналитики. Это упрощает навигацию и понимание платформенной поверхности KMM-проекта. Каждый actual-файл должен находиться в соответствующем source set'е: androidMain, iosMain, desktopMain и так далее.

Default-реализации через expect fun с actual fun, где actual использует common-код — распространённый антипаттерн. Если платформенная реализация не отличается от дефолтной, 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, а concrete классы создать в платформенных модулях. Фабрика или dependency injection контейнер предоставляют правильную реализацию в runtime. Этот подход лучше подходит для тестирования, так как интерфейс можно замокать.

Dependency Injection (Koin, Kodein) — более гибкий, но менее производительный подход. DI-контейнер настраивается отдельно для каждой платформы и предоставляет платформенные зависимости в common-код. В отличие от expect/actual, инъекция происходит в runtime, что даёт возможность подменять реализации для тестирования. С другой стороны, ошибки конфигурации DI обнаруживаются только при запуске, а не на этапе компиляции.

ПодходПроверка на этапе компиляцииГибкость тестированияRuntime-накладные расходы
expect/actualПолнаяНизкая (actual нельзя замокать)Нулевые (компиляционная связь)
Интерфейсы + фабрикаЧастичнаяВысокая (можно замокать)Минимальные (виртуальный вызов)
Dependency InjectionНет (runtime)ВысокаяСредние (DI-прокси)

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

Често задавани въпроси

Каква е разликата между expect/actual и интерфейсите?

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

Може ли да се използва 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-реализаций для всех целевых платформ, предотвращая runtime-сбои.
  • Рекомендуется минимизировать expect/actual и использовать интерфейсы с DI для бизнес-логики.
  • Организация кода должна быть единообразной с совпадающими именами файлов и пакетов для expect и actual.
  • Используйте expect/actual для низкоуровневых платформенных операций (хранилище, файловая система, датчики) — это обеспечивает нулевые runtime-накладные расходы.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също