expect/actual — essence, mots-clés KMM et leur fonctionnement

Auteur : IT Sectr Publié le : 2026-06-05 Temps de lecture : 8 min

expect/actual est un mécanisme de Kotlin Multiplatform qui permet de déclarer des API dépendantes de la plateforme dans du code commun. Le mot-clé expect crée un contrat de fonction, de classe ou de propriété dans commonMain, tandis que le mot-clé actual fournit une implémentation concrète pour chaque plateforme. Le compilateur vérifie que chaque déclaration expect a une implémentation actual correspondante sur toutes les plateformes cibles. Selon JetBrains, 2025, ce mécanisme est utilisé dans 80% des projets KMM pour implémenter de la logique métier multiplateforme.

Points clés

  • expect est le mot-clé pour déclarer un contrat de fonction, classe ou propriété dans le code commun.
  • actual est le mot-clé pour fournir une implémentation spécifique à une plateforme d’une déclaration expect.
  • commonMain est le source set avec le code commun où les déclarations expect sont placées.
  • Vérification du compilateur — le compilateur garantit que des implémentations actual existent pour toutes les plateformes cibles.
  • Source set — ensembles (iosMain, androidMain) où résident les implémentations actual spécifiques à la plateforme.

Qu’est-ce qu’expect/actual?

expect/actual est un mécanisme déclaratif de Kotlin Multiplatform pour implémenter la programmation orientée plateforme. Il permet de décrire une API une fois dans le module commun (expect) et de l’implémenter séparément pour chaque plateforme (actual). Contrairement aux interfaces, expect/actual ne crée pas d’appels virtuels — le compilateur lie les déclarations expect et actual à la compilation, éliminant ainsi la surcharge de la dispatch dynamique.

L’histoire d’expect/actual a commencé avec l’introduction de Kotlin Multiplatform en 2017. Au début, le mécanisme s’appelait expect/actual declarations et était expérimental. Dans Kotlin 1.2, les annotations expect ont été ajoutées, et dans Kotlin 1.3, expect/actual est devenu stable pour les classes et fonctions. Au fil du temps, le mécanisme a été étendu : Kotlin 1.6 a ajouté la prise en charge d’expect/actual pour les objets compagnons, Kotlin 1.7 pour les classes enum et Kotlin 2.0 pour les typealias.

La caractéristique clé d’expect/actual est la sécurité à la compilation. Si un développeur ajoute une déclaration expect dans commonMain mais oublie de fournir une implémentation actual pour iOS, le compilateur générera une erreur. Cela évite les échecs d’exécution courants dans les approches utilisant la réflexion ou le chargement dynamique de code plateforme.

Comment fonctionne le mécanisme expect/actual

Le mécanisme d’expect/actual fonctionne au niveau du source set — le système de modules de Kotlin Multiplatform. Le code commun disponible pour toutes les plateformes réside dans le source set commonMain. Le code dépendant de la plateforme réside dans iosMain, androidMain, macosMain, etc. Le mot-clé expect dans commonMain déclare une API, tandis que le mot-clé actual dans un source set de plateforme fournit l’implémentation. Le compilateur les lie à l’étape de génération de code, remplaçant l’appel de fonction expect par l’implémentation actual correspondante pour la plateforme cible.

La hiérarchie des source sets dans un projet KMM typique est la suivante : commonMain contient les déclarations expect, iosMain et androidMain contiennent les implémentations actual. Lors de la compilation pour iOS, l’actual d’iosMain est utilisé ; lors de la compilation pour Android, l’actual d’androidMain est utilisé. Les source sets peuvent être intermédiaires (par exemple, iosArm64Main pour une architecture spécifique), ce qui permet d’affiner les implémentations pour différents appareils.

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"

Vérification par le compilateur des implémentations actual

Le compilateur Kotlin vérifie plusieurs conditions lors du travail avec expect/actual. Chaque déclaration expect doit avoir une implémentation actual pour chaque plateforme active. La signature de la déclaration actual doit correspondre à la signature expect (l’annotation @OptionalExpectation peut assouplir cette exigence). Les modificateurs d’accès, le type de retour et les paramètres doivent être identiques. Le compilateur vérifie également l’absence de dépendances cycliques entre les déclarations expect et actual.

Types d’expect/actual : fonctions, classes, propriétés

expect/actual prend en charge plusieurs types de déclarations. Les plus couramment utilisés sont les fonctions expect/actual pour les opérations de plateforme, les classes expect/actual pour les objets nécessitant une implémentation native et les propriétés expect/actual pour les constantes et configurations. Chaque type a ses propres règles d’utilisation et limitations.

Les fonctions expect/actual sont le type le plus simple et le plus courant. Elles sont utilisées pour appeler des API de plateforme telles que l’obtention de l’heure, la lecture de fichiers ou l’envoi de requêtes HTTP. Les classes expect/actual sont utilisées pour créer des objets qui interagissent directement avec du code natif (par exemple, pour accéder à l’appareil photo, à la géolocalisation ou au stockage de clés). Les propriétés expect/actual (val) conviennent aux constantes de plateforme — nom de l’OS, version du SDK ou chemin du répertoire système.

Type de déclarationMots-clésExemple d’utilisation
Fonctionexpect fun / actual funObtenir un identifiant unique d’appareil
Classeexpect class / actual classAccéder à SecureStorage (Keychain / EncryptedSharedPreferences)
Propriétéexpect val / actual valPlateforme actuelle (iOS / Android)
Enum classexpect enum / actual enumListe des autorisations d’application disponibles
Typealiasexpect typealias / actual typealiasType de réponse réseau spécifique à la plateforme

Limitations d’expect/actual

Toutes les constructions Kotlin ne peuvent pas être utilisées avec expect/actual. Une déclaration expect ne peut pas contenir de corps — seulement une signature. Une classe expect ne peut pas avoir de constructeur avec paramètres (elle doit avoir un constructeur primaire vide). Pour enum expect/actual, toutes les constantes doivent être identiques dans expect et actual. Les propriétés expect doivent être val (pas var), car stocker un état dans le module commun pour des propriétés de plateforme n’a pas de sens.

Exemples de code : du simple au complexe

Explorons des exemples pratiques d’expect/actual, des fonctions simples aux classes complètes. Le cas de base est l’obtention du nom de la plateforme pour l’utiliser dans l’interface utilisateur. Les exemples plus complexes incluent l’accès au stockage natif et le travail avec les threads de plateforme.

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

Dans cet exemple, la classe expect PlatformStorage définit le contrat d’un stockage simple clé-valeur. Sur Android, l’implémentation utilise SharedPreferences, tandis que sur iOS elle utilise Keychain ou NSUserDefaults. Grâce à expect/actual, la logique métier dans commonMain appelle save/get/remove sans connaître l’implémentation de la plateforme.

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

Bonnes pratiques d’expect/actual

Lors de la conception d’API expect/actual, plusieurs principes doivent être suivis. Minimisez le nombre de déclarations expect — plus il y a de code commun, plus la maintenance est simple. Utilisez expect/actual uniquement pour les API qui diffèrent réellement entre les plateformes. Pour le reste du code, utilisez des interfaces avec des fabriques ou l’injection de dépendances, ce qui simplifie les tests.

Il est recommandé de regrouper les déclarations expect par modules thématiques, plutôt que de les mélanger dans un seul fichier. Par exemple, Storage.kt pour les déclarations expect liées au stockage, Platform.kt pour les fonctions expect travaillant avec l’OS et Analytics.kt pour les classes expect d’analyse. Cela simplifie la navigation et la compréhension de la surface de plateforme d’un projet KMM. Chaque fichier actual doit être dans le source set correspondant : androidMain, iosMain, desktopMain, etc.

Les implémentations par défaut via expect fun avec actual fun où actual utilise du code commun est un antipattern courant. Si l’implémentation de la plateforme ne diffère pas de la valeur par défaut, expect/actual n’est pas nécessaire. Dans ces cas, utilisez une fonction simple dans commonMain. Évitez également expect/actual pour les getters triviaux — utilisez expect val avec des constantes.

Organisation du code dans un projet

La structure appropriée du code expect/actual est cruciale pour la lisibilité du projet. Chaque module expect/actual doit avoir un point d’entrée unique. Exemple d’organisation : commonMain/kotlin/com/project/platform contient les déclarations expect, androidMain/kotlin/com/project/platform contient actual pour Android, iosMain/kotlin/com/project/platform contient actual pour iOS. Les noms de fichiers et de packages doivent correspondre pour expect et actual, afin qu’un développeur puisse trouver rapidement l’implémentation correspondante.

Alternatives à expect/actual dans KMM

Les interfaces avec une fabrique de plateforme sont la principale alternative à expect/actual. Au lieu d’une classe expect, vous pouvez déclarer une interface dans commonMain et créer des classes concrètes dans les modules de plateforme. Une fabrique ou un conteneur d’injection de dépendances fournit l’implémentation correcte à l’exécution. Cette approche est meilleure pour les tests, car l’interface peut être mockée.

L’injection de dépendances (Koin, Kodein) est une approche plus flexible mais moins performante. Un conteneur DI est configuré séparément pour chaque plateforme et fournit les dépendances de plateforme au code commun. Contrairement à expect/actual, l’injection se produit à l’exécution, ce qui permet d’échanger les implémentations pour les tests. D’un autre côté, les erreurs de configuration DI ne sont détectées qu’à l’exécution, pas à la compilation.

ApprocheVérification à la compilationFlexibilité de testSurcharge d’exécution
expect/actualComplèteFaible (actual ne peut pas être mocké)Nulle (liaison à la compilation)
Interfaces + FabriquePartielleÉlevée (peut être mockée)Minime (appel virtuel)
Injection de dépendancesNon (exécution)ÉlevéeModérée (proxies DI)

Le choix entre expect/actual et les alternatives dépend du contexte. Pour le code critique en performance (moteurs de jeu, traitement en temps réel), expect/actual est préférable en raison de sa surcharge nulle. Pour la logique métier (référentiels, cas d’utilisation), il est préférable d’utiliser des interfaces avec DI pour simplifier les tests. Une approche combinée — expect/actual pour les opérations de bas niveau de plateforme et des interfaces pour la couche de logique métier — est utilisée dans la plupart des projets KMM de production.

Questions fréquentes

Quelle est la différence entre expect/actual et les interfaces?

expect/actual lie l’implémentation à la compilation sans appels virtuels, tandis que les interfaces lient à l’exécution. expect/actual garantit une implémentation pour toutes les plateformes, les interfaces nécessitent des vérifications à l’exécution.

Peut-on utiliser expect/actual pour les enums?

Oui, expect enum est pris en charge depuis Kotlin 1.7. Toutes les constantes dans les enums expect et actual doivent correspondre. Des valeurs de constantes différentes sur différentes plateformes constituent une erreur de compilation.

Que se passe-t-il si une implémentation actual est manquante?

Le compilateur génère une erreur pour chaque plateforme où l’implémentation actual est manquante. Le projet ne se compilera pas tant que des implémentations actual correspondantes ne seront pas ajoutées pour toutes les déclarations expect.

Peut-on utiliser expect/actual dans un seul source set?

Non, expect et actual doivent être dans des source sets différents. expect dans commonMain ou un source set intermédiaire, actual dans un source set de plateforme. Placer expect et actual dans le même source set est une erreur de compilation.

Comment tester le code expect/actual?

Pour tester expect/actual, utilisez commonTest avec des source sets de test de plateforme. Écrivez des tests expect dans commonTest et des tests actual pour chaque plateforme. Les tests d’intégration sont exécutés séparément sur chaque plateforme cible.

Résumé

  • expect/actual est le mécanisme clé de Kotlin Multiplatform pour les implémentations de plateforme avec vérification du compilateur.
  • expect déclare un contrat dans commonMain, actual fournit l’implémentation dans un source set de plateforme.
  • Les types de déclaration incluent les fonctions, classes, propriétés, classes enum et typealias avec différentes règles d’utilisation.
  • La vérification du compilateur garantit des implémentations actual pour toutes les plateformes cibles, empêchant les échecs d’exécution.
  • Il est recommandé de minimiser expect/actual et d’utiliser des interfaces avec DI pour la logique métier.
  • L’organisation du code doit être cohérente avec des noms de fichiers et de packages correspondant pour expect et actual.
  • Utilisez expect/actual pour les opérations de bas niveau de plateforme (stockage, système de fichiers, capteurs) — cela garantit une surcharge d’exécution nulle.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi