expect/actual — essenza, parole chiave KMM e come funzionano

Autore: IT Sectr Pubblicato: 2026-06-05 Tempo di lettura: 8 min

expect/actual è un meccanismo di Kotlin Multiplatform che consente di dichiarare API dipendenti dalla piattaforma in codice comune. La parola chiave expect crea un contratto di funzione, classe o proprietà in commonMain, mentre la parola chiave actual fornisce un'implementazione concreta per ogni piattaforma. Il compilatore verifica che ogni dichiarazione expect abbia un'implementazione actual corrispondente su tutte le piattaforme target. Secondo JetBrains, 2025, questo meccanismo è utilizzato nell'80% dei progetti KMM per implementare logica di business multipiattaforma.

Punti chiave

  • expect è la parola chiave per dichiarare un contratto di funzione, classe o proprietà in codice comune.
  • actual è la parola chiave per fornire un'implementazione specifica della piattaforma di una dichiarazione expect.
  • commonMain è il source set con codice comune dove sono posizionate le dichiarazioni expect.
  • Verifica del compilatore — il compilatore garantisce che esistano implementazioni actual per tutte le piattaforme target.
  • Source set — insiemi (iosMain, androidMain) dove risiedono le implementazioni actual specifiche della piattaforma.

Cos'è expect/actual?

expect/actual è un meccanismo dichiarativo di Kotlin Multiplatform per implementare la programmazione orientata alla piattaforma. Consente di descrivere un'API una volta nel modulo comune (expect) e implementarla separatamente per ogni piattaforma (actual). A differenza delle interfacce, expect/actual non crea chiamate virtuali — il compilatore collega le dichiarazioni expect e actual al momento della compilazione, eliminando l'overhead del dispatch dinamico.

La storia di expect/actual è iniziata con l'introduzione di Kotlin Multiplatform nel 2017. Inizialmente, il meccanismo si chiamava expect/actual declarations ed era sperimentale. In Kotlin 1.2 sono state aggiunte le annotazioni expect, e in Kotlin 1.3 expect/actual è diventato stabile per classi e funzioni. Col tempo il meccanismo si è ampliato: Kotlin 1.6 ha aggiunto il supporto per expect/actual per gli oggetti companion, Kotlin 1.7 per le classi enum e Kotlin 2.0 per typealias.

La caratteristica principale di expect/actual è la sicurezza al momento della compilazione. Se uno sviluppatore aggiunge una dichiarazione expect in commonMain ma dimentica di fornire un'implementazione actual per iOS, il compilatore genererà un errore. Ciò previene gli errori di runtime comuni negli approcci che utilizzano la riflessione o il caricamento dinamico del codice della piattaforma.

Come funziona il meccanismo expect/actual

Il meccanismo di expect/actual funziona a livello di source set — il sistema di moduli di Kotlin Multiplatform. Il codice comune disponibile per tutte le piattaforme risiede nel source set commonMain. Il codice dipendente dalla piattaforma risiede in iosMain, androidMain, macosMain, ecc. La parola chiave expect in commonMain dichiara un'API, mentre la parola chiave actual in un source set di piattaforma fornisce l'implementazione. Il compilatore le collega nella fase di generazione del codice, sostituendo la chiamata alla funzione expect con l'implementazione actual corrispondente per la piattaforma target.

La gerarchia dei source set in un tipico progetto KMM è la seguente: commonMain contiene le dichiarazioni expect, iosMain e androidMain contengono le implementazioni actual. Durante la compilazione per iOS, viene utilizzato l'actual di iosMain; durante la compilazione per Android, l'actual di androidMain. I source set possono essere intermedi (ad esempio, iosArm64Main per un'architettura specifica), consentendo di perfezionare le implementazioni per diversi dispositivi.

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"

Verifica del compilatore delle implementazioni actual

Il compilatore Kotlin verifica diverse condizioni quando lavora con expect/actual. Ogni dichiarazione expect deve avere un'implementazione actual per ogni piattaforma attiva. La firma della dichiarazione actual deve corrispondere alla firma expect (l'annotazione @OptionalExpectation può allentare questo requisito). I modificatori di accesso, il tipo restituito e i parametri devono essere identici. Il compilatore verifica anche l'assenza di dipendenze cicliche tra le dichiarazioni expect e actual.

Tipi di expect/actual: funzioni, classi, proprietà

expect/actual supporta diversi tipi di dichiarazioni. I più comunemente utilizzati sono le funzioni expect/actual per operazioni di piattaforma, le classi expect/actual per oggetti che richiedono implementazione nativa e le proprietà expect/actual per costanti e impostazioni. Ogni tipo ha le proprie regole di utilizzo e limitazioni.

Le funzioni expect/actual sono il tipo più semplice e comune. Vengono utilizzate per chiamare API di piattaforma come ottenere l'ora, leggere file o inviare richieste HTTP. Le classi expect/actual vengono utilizzate per creare oggetti che interagiscono direttamente con codice nativo (ad esempio, per accedere alla fotocamera, alla geolocalizzazione o all'archiviazione delle chiavi). Le proprietà expect/actual (val) sono adatte per costanti di piattaforma — nome del sistema operativo, versione SDK o percorso della directory di sistema.

Tipo di dichiarazioneParole chiaveEsempio di utilizzo
Funzioneexpect fun / actual funOttenere un identificatore univoco del dispositivo
Classeexpect class / actual classAccedere a SecureStorage (Keychain / EncryptedSharedPreferences)
Proprietàexpect val / actual valPiattaforma corrente (iOS / Android)
Enum classexpect enum / actual enumElenco delle autorizzazioni dell'app disponibili
Typealiasexpect typealias / actual typealiasTipo di risposta di rete specifico della piattaforma

Limitazioni di expect/actual

Non tutti i costrutti Kotlin possono essere utilizzati con expect/actual. Una dichiarazione expect non può contenere un corpo — solo una firma. Una classe expect non può avere un costruttore con parametri (deve avere un costruttore primario vuoto). Per enum expect/actual, tutte le costanti devono essere identiche sia in expect che in actual. Le proprietà expect devono essere val (non var), poiché memorizzare lo stato nel modulo comune per proprietà di piattaforma non ha senso.

Esempi di codice: dal semplice al complesso

Esploriamo esempi pratici di expect/actual, dalle funzioni semplici alle classi complete. Il caso base è ottenere il nome della piattaforma per l'uso nell'interfaccia utente. Esempi più complessi includono l'accesso all'archiviazione nativa e il lavoro con i thread della piattaforma.

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

In questo esempio, la classe expect PlatformStorage definisce il contratto di un semplice archivio chiave-valore. Su Android, l'implementazione utilizza SharedPreferences, mentre su iOS utilizza Keychain o NSUserDefaults. Grazie a expect/actual, la logica di business in commonMain chiama save/get/remove senza conoscere l'implementazione della piattaforma.

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

Migliori pratiche di expect/actual

Durante la progettazione di API expect/actual, è necessario seguire diversi principi. Minimizzare il numero di dichiarazioni expect — più codice comune, più semplice è la manutenzione. Utilizzare expect/actual solo per API che differiscono effettivamente tra le piattaforme. Per il resto del codice, utilizzare interfacce con fabbriche o dependency injection, che semplificano i test.

Si consiglia di raggruppare le dichiarazioni expect per moduli tematici, invece di mescolarle in un unico file. Ad esempio, Storage.kt per le dichiarazioni expect relative all'archiviazione, Platform.kt per le funzioni expect che lavorano con il sistema operativo e Analytics.kt per le classi expect di analisi. Ciò semplifica la navigazione e la comprensione della superficie della piattaforma di un progetto KMM. Ogni file actual deve trovarsi nel source set corrispondente: androidMain, iosMain, desktopMain, ecc.

Le implementazioni predefinite tramite expect fun con actual fun dove actual utilizza codice comune è un antipattern comune. Se l'implementazione della piattaforma non differisce da quella predefinita, expect/actual non è necessario. In questi casi, utilizzare una funzione semplice in commonMain. Evitare anche expect/actual per getter banali — utilizzare expect val con costanti.

Organizzazione del codice in un progetto

La struttura corretta del codice expect/actual è fondamentale per la leggibilità del progetto. Ogni modulo expect/actual dovrebbe avere un unico punto di ingresso. Esempio di organizzazione: commonMain/kotlin/com/project/platform contiene le dichiarazioni expect, androidMain/kotlin/com/project/platform contiene actual per Android, iosMain/kotlin/com/project/platform contiene actual per iOS. I nomi di file e pacchetti devono corrispondere per expect e actual, in modo che uno sviluppatore possa trovare rapidamente l'implementazione corrispondente.

Alternative a expect/actual in KMM

Le interfacce con una factory di piattaforma sono la principale alternativa a expect/actual. Invece di una classe expect, è possibile dichiarare un'interfaccia in commonMain e creare classi concrete nei moduli di piattaforma. Una factory o un contenitore di dependency injection fornisce l'implementazione corretta al runtime. Questo approccio è migliore per i test, poiché l'interfaccia può essere mockata.

La dependency injection (Koin, Kodein) è un approccio più flessibile ma meno efficiente. Un contenitore DI viene configurato separatamente per ogni piattaforma e fornisce le dipendenze della piattaforma al codice comune. A differenza di expect/actual, l'iniezione avviene al runtime, consentendo di scambiare le implementazioni per i test. D'altra parte, gli errori di configurazione DI vengono rilevati solo al runtime, non al momento della compilazione.

ApproccioVerifica alla compilazioneFlessibilità di testOverhead runtime
expect/actualCompletaBassa (actual non può essere mockato)Zero (legame alla compilazione)
Interfacce + FactoryParzialeAlta (può essere mockato)Minima (chiamata virtuale)
Dependency InjectionNo (runtime)AltaModerata (proxy DI)

La scelta tra expect/actual e alternative dipende dal contesto. Per il codice critico per le prestazioni(motori di gioco, elaborazione in tempo reale), expect/actual è preferibile grazie all'overhead zero. Per la logica di business (repository, casi d'uso), è meglio utilizzare interfacce con DI per semplificare i test. Un approccio combinato — expect/actual per operazioni di piattaforma di basso livello e interfacce per il livello di logica di business — è utilizzato nella maggior parte dei progetti KMM di produzione.

Domande frequenti

Qual è la differenza tra expect/actual e le interfacce?

expect/actual lega l'implementazione al momento della compilazione senza chiamate virtuali, mentre le interfacce legano al runtime. expect/actual garantisce l'implementazione per tutte le piattaforme, le interfacce richiedono controlli al runtime.

Si può usare expect/actual per gli enum?

Sì, expect enum è supportato a partire da Kotlin 1.7. Tutte le costanti negli enum expect e actual devono corrispondere. Valori di costanti diversi su piattaforme diverse sono un errore di compilazione.

Cosa succede se manca un'implementazione actual?

Il compilatore genererà un errore per ogni piattaforma in cui manca l'implementazione actual. Il progetto non verrà compilato fino a quando non verranno aggiunte le corrispondenti implementazioni actual per tutte le dichiarazioni expect.

Si può usare expect/actual all'interno di un singolo source set?

No, expect e actual devono essere in source set diversi. expect in commonMain o in un source set intermedio, actual in un source set di piattaforma. Mettere expect e actual nello stesso source set è un errore di compilazione.

Come testare il codice expect/actual?

Per testare expect/actual, utilizzare commonTest con source set di test di piattaforma. Scrivere test expect in commonTest e test actual per ogni piattaforma. I test di integrazione vengono eseguiti separatamente su ciascuna piattaforma target.

Riepilogo

  • expect/actual è il meccanismo chiave di Kotlin Multiplatform per implementazioni di piattaforma con verifica del compilatore.
  • expect dichiara un contratto in commonMain, actual fornisce l'implementazione in un source set di piattaforma.
  • I tipi di dichiarazione includono funzioni, classi, proprietà, classi enum e typealias con diverse regole di utilizzo.
  • La verifica del compilatore garantisce implementazioni actual per tutte le piattaforme target, prevenendo errori di runtime.
  • Si raccomanda di minimizzare expect/actual e utilizzare interfacce con DI per la logica di business.
  • L'organizzazione del codice deve essere coerente con nomi di file e pacchetti corrispondenti per expect e actual.
  • Utilizzare expect/actual per operazioni di piattaforma di basso livello (archiviazione, file system, sensori) — ciò garantisce overhead runtime zero.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche