expect/actual — istota, słowa kluczowe KMM i jak działają

Autor: IT Sectr Opublikowano: 2026-06-05 Czas czytania: 8 min

expect/actual — mechanizm Kotlin Multiplatform, który pozwala deklarować API zależne od platformy w kodzie wspólnym. Słowo kluczowe expect tworzy kontrakt funkcji, klasy lub właściwości w commonMain, a słowo kluczowe actual dostarcza konkretną implementację dla każdej platformy. Kompilator sprawdza, czy dla każdej deklaracji expect istnieje implementacja actual na wszystkich docelowych platformach. Według danych JetBrains, 2025, mechanizm jest używany w 80% projektów KMM do implementacji platformowej logiki biznesowej.

Najważniejsze

  • expect — słowo kluczowe do deklarowania kontraktu funkcji, klasy lub właściwości we wspólnym kodzie.
  • actual — słowo kluczowe do dostarczenia platformowej implementacji deklaracji expect.
  • commonMain — source set z kodem wspólnym, gdzie znajdują się deklaracje expect.
  • Sprawdzenie kompilatora — kompilator gwarantuje istnienie implementacji actual dla wszystkich docelowych platform.
  • Source set — zestawy (iosMain, androidMain), gdzie znajdują się platformowe implementacje actual.

Czym jest expect/actual?

expect/actual — to deklaratywny mechanizm Kotlin Multiplatform do implementacji programowania zorientowanego na platformę. Pozwala opisać API raz w module wspólnym (expect) i zaimplementować je osobno dla każdej platformy (actual). W przeciwieństwie do interfejsów, expect/actual nie tworzy wywołań wirtualnych — kompilator wiąże deklaracje expect i actual na etapie kompilacji, co eliminuje narzuty na dynamiczną dyspozycję.

Historia expect/actual rozpoczęła się wraz z pojawieniem się Kotlin Multiplatform w 2017 roku. Pierwotnie mechanizm nazywał się expect/actual declarations i był eksperymentalny. W Kotlin 1.2 dodano adnotacje expect, a w Kotlin 1.3 expect/actual stał się stabilny dla klas i funkcji. Stopniowo mechanizm był rozszerzany: w Kotlin 1.6 dodano obsługę expect/actual dla obiektów companion, w Kotlin 1.7 — dla klas enum, a w Kotlin 2.0 — dla typealias.

Kluczowa cecha expect/actual — bezpieczeństwo na poziomie kompilacji. Jeśli programista dodał deklarację expect w commonMain, ale zapomniał dostarczyć implementacji actual dla iOS, kompilator zgłosi błąd. Zapobiega to błędom runtime, charakterystycznym dla podejść z refleksją lub dynamicznym ładowaniem kodu platformowego.

Jak działa mechanizm expect/actual

Mechanizm expect/actual działa na poziomie source set — systemu modułów Kotlin Multiplatform. Kod wspólny, dostępny dla wszystkich platform, znajduje się w source set commonMain. Kod zależny od platformy — w iosMain, androidMain, macosMain i tak dalej. Słowo kluczowe expect w commonMain deklaruje API, a słowo kluczowe actual w platformowym source set dostarcza implementację. Kompilator wiąże je na etapie generowania kodu, zastępując wywołanie funkcji expect odpowiednią implementacją actual dla docelowej platformy.

Hierarchia source set w typowym projekcie KMM wygląda następująco: commonMain zawiera deklaracje expect, iosMain i androidMain zawierają implementacje actual. Podczas kompilacji dla iOS używane jest actual z iosMain, podczas kompilacji dla Android — z androidMain. Source set mogą być pośrednie (na przykład iosArm64Main dla konkretnej architektury), co pozwala doprecyzować implementacje dla różnych urządzeń.

kotlin
// commonMain — deklaracja expect
expect fun getPlatformName(): String

// androidMain — actual dla Android
actual fun getPlatformName(): String = "Android"

// iosMain — actual dla iOS
actual fun getPlatformName(): String = "iOS"

Sprawdzenie implementacji actual przez kompilator

Kompilator Kotlin sprawdza kilka warunków podczas pracy z expect/actual. Każda deklaracja expect musi mieć implementację actual dla każdej aktywnej platformy. Sygnatura deklaracji actual musi zgadzać się z sygnaturą expect (adnotacja @OptionalExpectation może łagodzić to wymaganie). Modyfikatory dostępu, typ zwracany i parametry muszą być identyczne. Kompilator sprawdza również brak cyklicznych zależności między deklaracjami expect i actual.

Typy expect/actual: funkcje, klasy, właściwości

expect/actual obsługuje kilka typów deklaracji. Najczęściej używane są funkcje expect/actual do operacji platformowych, klasy expect/actual dla obiektów wymagających natywnej implementacji oraz właściwości expect/actual dla stałych i ustawień. Każdy typ ma swoje zasady użycia i ograniczenia.

Funkcje expect/actual — najprostszy i najczęstszy typ. Są używane do wywoływania platformowych API, takich jak pobieranie czasu, odczytywanie plików czy wysyłanie zapytań HTTP. Klasy expect/actual są stosowane do tworzenia obiektów, które bezpośrednio współdziałają z kodem natywnym (na przykład do dostępu do kamery, geolokalizacji lub magazynu kluczy). Właściwości expect/actual (val) nadają się do stałych platformowych — nazwy systemu operacyjnego, wersji SDK lub ścieżki do katalogu systemowego.

Typ deklaracjiSłowa kluczowePrzykład użycia
Funkcjaexpect fun / actual funPobieranie unikalnego identyfikatora urządzenia
Klasaexpect class / actual classDostęp do SecureStorage (Keychain / EncryptedSharedPreferences)
Właściwośćexpect val / actual valBieżąca platforma (iOS / Android)
Klasa enumexpect enum / actual enumLista dostępnych uprawnień aplikacji
Typealiasexpect typealias / actual typealiasTyp odpowiedzi sieciowej specyficzny dla platformy

Ograniczenia expect/actual

Nie wszystkie konstrukcje Kotlin mogą być użyte z expect/actual. Deklaracja expect nie może zawierać ciała — tylko sygnaturę. Klasa expect nie może mieć konstruktora z parametrami (musi mieć pusty konstruktor podstawowy). Dla enum expect/actual wszystkie stałe muszą być identyczne w expect i actual. Właściwości expect muszą być val (nie var), ponieważ przechowywanie stanu w module wspólnym dla właściwości platformowych nie ma sensu.

Przykłady kodu: od prostego do złożonego

Rozważmy praktyczne przykłady expect/actual od prostych funkcji po pełnoprawne klasy. Podstawowy przypadek — pobieranie nazwy platformy do użycia w interfejsie. Bardziej złożone przykłady obejmują dostęp do natywnego magazynu i pracę z platformowymi wątkami.

kotlin
// commonMain — klasa expect dla bezpiecznego przechowywania
expect class PlatformStorage {
    fun save(key: String, value: String)
    fun get(key: String): String?
    fun remove(key: String)
}

// androidMain — actual na Androidzie
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() }
}

W tym przykładzie klasa expect PlatformStorage określa kontrakt prostego magazynu klucz-wartość. Na Androidzie implementacja używa SharedPreferences, a na iOS — Keychain lub NSUserDefaults. Dzięki expect/actual logika biznesowa w commonMain wywołuje save/get/remove, nie znając platformowej implementacji.

kotlin
// iosMain — actual na iOS z 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)
    }
}

Najlepsze praktyki expect/actual

Projektując API expect/actual, należy przestrzegać kilku zasad. Minimalizuj liczbę deklaracji expect — im więcej wspólnego kodu, tym łatwiejsze utrzymanie. Używaj expect/actual tylko dla API, które rzeczywiście różnią się na platformach. Dla pozostałego kodu stosuj interfejsy z fabrykami lub wstrzykiwaniem zależności, co upraszcza testowanie.

Zaleca się grupowanie deklaracji expect według modułów tematycznych, a nie mieszanie ich w jednym pliku. Na przykład Storage.kt dla deklaracji expect magazynu, Platform.kt dla funkcji expect pracy z systemem operacyjnym i Analytics.kt dla klas expect analityki. Upraszcza to nawigację i zrozumienie platformowej powierzchni projektu KMM. Każdy plik actual powinien znajdować się w odpowiednim source set: androidMain, iosMain, desktopMain i tak dalej.

Implementacje domyślne przez expect fun z actual fun, gdzie actual używa kodu wspólnego — to częsty antywzorzec. Jeśli platformowa implementacja nie różni się od domyślnej, expect/actual nie jest potrzebny. W takich przypadkach używaj prostej funkcji w commonMain. Unikaj również expect/actual dla trywialnych getterów — używaj expect val ze stałymi.

Organizacja kodu w projekcie

Prawidłowa struktura kodu expect/actual jest krytyczna dla czytelności projektu. Każdy moduł expect/actual powinien mieć jeden punkt wejścia. Przykład organizacji: commonMain/kotlin/com/project/platform zawiera deklaracje expect, androidMain/kotlin/com/project/platform — actual dla Androida, iosMain/kotlin/com/project/platform — actual dla iOS. Nazwy plików i pakietów powinny być zgodne dla expect i actual, aby programista mógł szybko znaleźć odpowiednią implementację.

Alternatywy dla expect/actual w KMM

Interfejsy z platformową fabryką — główna alternatywa dla expect/actual. Zamiast klasy expect można zadeklarować interfejs w commonMain, a konkretne klasy utworzyć w modułach platformowych. Fabryka lub kontener wstrzykiwania zależności dostarcza odpowiednią implementację w runtime. To podejście lepiej nadaje się do testowania, ponieważ interfejs można zamockować.

Wstrzykiwanie zależności (Koin, Kodein) — bardziej elastyczne, ale mniej wydajne podejście. Kontener DI jest konfigurowany oddzielnie dla każdej platformy i dostarcza platformowe zależności do wspólnego kodu. W przeciwieństwie do expect/actual, wstrzyknięcie następuje w runtime, co umożliwia podmianę implementacji do testowania. Z drugiej strony, błędy konfiguracji DI są wykrywane dopiero przy uruchomieniu, a nie na etapie kompilacji.

PodejścieSprawdzenie na etapie kompilacjiElastyczność testowaniaNarzut runtime
expect/actualPełneNiska (actual nie można zamockować)Zerowy (wiązanie kompilacyjne)
Interfejsy + fabrykaczęścioweWysoka (można zamockować)Minimalne (wywołanie wirtualne)
Wstrzykiwanie zależnościNie (runtime)WysokaŚrednie (proxy DI)

Wybór między expect/actual a alternatywami zależy od kontekstu. Dla krytycznej wydajności (silniki gier, przetwarzanie w czasie rzeczywistym) expect/actual jest preferowany ze względu na zerowy narzut runtime. Dla logiki biznesowej (repozytoria, przypadki użycia) lepiej stosować interfejsy z DI, aby uprościć testowanie. Podejście łączone — expect/actual dla niskopoziomowych operacji platformowych i interfejsy dla warstwy logiki biznesowej — jest stosowane w większości produkcyjnych projektów KMM.

Często zadawane pytania

Czym różni się expect/actual od interfejsów?

expect/actual wiąże implementację na etapie kompilacji bez wywołań wirtualnych, a interfejsy — w runtime. expect/actual gwarantuje istnienie implementacji dla wszystkich platform, interfejsy wymagają sprawdzeń w runtime.

Czy można używać expect/actual dla enum?

Tak, expect enum jest obsługiwany od Kotlin 1.7. Wszystkie stałe w expect i actual enum muszą być zgodne. Różne wartości stałych na różnych platformach — błąd kompilacji.

Co się stanie, jeśli zapomni się o implementacji actual?

Kompilator zgłosi błąd dla każdej platformy, dla której brakuje implementacji actual. Projekt nie zostanie zbudowany, dopóki dla wszystkich deklaracji expect nie zostaną dodane odpowiednie implementacje actual.

Czy można używać expect/actual w jednym source set?

Nie, expect i actual muszą znajdować się w różnych source set. expect — w commonMain lub pośrednim source set, actual — w platformowym source set. Umieszczenie expect i actual w jednym source set to błąd kompilacji.

Jak testować kod expect/actual?

Do testowania expect/actual używaj commonTest z platformowymi testowymi source set. Napisz testy expect w commonTest i testy actual dla każdej platformy. Testy integracyjne uruchamiane są osobno na każdej docelowej platformie.

Podsumowanie

  • expect/actual — kluczowy mechanizm Kotlin Multiplatform dla implementacji platformowych ze sprawdzeniem kompilatora.
  • expect deklaruje kontrakt w commonMain, actual dostarcza implementację w platformowym source set.
  • Typy deklaracji obejmują funkcje, klasy, właściwości, klasy enum i typealias z różnymi zasadami użycia.
  • Sprawdzenie kompilatora gwarantuje istnienie implementacji actual dla wszystkich docelowych platform, zapobiegając błędom runtime.
  • Zaleca się minimalizować expect/actual i stosować interfejsy z DI dla logiki biznesowej.
  • Organizacja kodu powinna być jednolita z zgodnymi nazwami plików i pakietów dla expect i actual.
  • Używaj expect/actual dla niskopoziomowych operacji platformowych (magazyn, system plików, czujniki) — zapewnia to zerowy narzut runtime.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również