expect/actual es un mecanismo de Kotlin Multiplatform que permite declarar API dependientes de la plataforma en código común. La palabra clave expect crea un contrato de función, clase o propiedad en commonMain, mientras que la palabra clave actual proporciona una implementación concreta para cada plataforma. El compilador verifica que cada declaración expect tenga una implementación actual correspondiente en todas las plataformas objetivo. Según JetBrains, 2025, este mecanismo se utiliza en el 80% de los proyectos KMM para implementar lógica de negocio multiplataforma.
Puntos clave
expect/actual es un mecanismo declarativo de Kotlin Multiplatform para implementar programación orientada a la plataforma. Permite describir una API una vez en el módulo común (expect) e implementarla por separado para cada plataforma (actual). A diferencia de las interfaces, expect/actual no crea llamadas virtuales — el compilador vincula las declaraciones expect y actual en tiempo de compilación, eliminando la sobrecarga de la despacho dinámico.
La historia de expect/actual comenzó con la introducción de Kotlin Multiplatform en 2017. Inicialmente, el mecanismo se llamaba expect/actual declarations y era experimental. En Kotlin 1.2 se añadieron las anotaciones expect, y en Kotlin 1.3 expect/actual se volvió estable para clases y funciones. Con el tiempo, el mecanismo se amplió: Kotlin 1.6 añadió soporte para expect/actual en companion objects, Kotlin 1.7 para enum classes y Kotlin 2.0 para typealias.
La característica clave de expect/actual es la seguridad en tiempo de compilación. Si un desarrollador añade una declaración expect en commonMain pero olvida proporcionar una implementación actual para iOS, el compilador generará un error. Esto evita fallos en tiempo de ejecución típicos de enfoques que usan reflexión o carga dinámica de código de plataforma.
El mecanismo de expect/actual funciona a nivel de source set — el sistema de módulos de Kotlin Multiplatform. El código común disponible para todas las plataformas reside en el source set commonMain. El código dependiente de la plataforma reside en iosMain, androidMain, macosMain, etc. La palabra clave expect en commonMain declara una API, mientras que la palabra clave actual en un source set de plataforma proporciona la implementación. El compilador las vincula en la etapa de generación de código, reemplazando la llamada a la función expect por la implementación actual correspondiente para la plataforma objetivo.
La jerarquía de source sets en un proyecto KMM típico es la siguiente: commonMain contiene las declaraciones expect, iosMain y androidMain contienen las implementaciones actual. Al compilar para iOS, se usa el actual de iosMain; al compilar para Android, el actual de androidMain. Los source sets pueden ser intermedios (por ejemplo, iosArm64Main para una arquitectura específica), lo que permite refinar las implementaciones para diferentes dispositivos.
// 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"
El compilador de Kotlin verifica varias condiciones al trabajar con expect/actual. Cada declaración expect debe tener una implementación actual para cada plataforma activa. La firma de la declaración actual debe coincidir con la firma expect (la anotación @OptionalExpectation puede relajar este requisito). Los modificadores de acceso, el tipo de retorno y los parámetros deben ser idénticos. El compilador también verifica que no haya dependencias cíclicas entre las declaraciones expect y actual.
expect/actual admite varios tipos de declaraciones. Los más utilizados son las funciones expect/actual para operaciones de plataforma, las clases expect/actual para objetos que requieren implementación nativa y las propiedades expect/actual para constantes y configuraciones. Cada tipo tiene sus propias reglas de uso y limitaciones.
Las funciones expect/actual son el tipo más simple y común. Se utilizan para llamar a API de plataforma como obtener la hora, leer archivos o enviar solicitudes HTTP. Las clases expect/actual se utilizan para crear objetos que interactúan directamente con código nativo (por ejemplo, para acceder a la cámara, geolocalización o almacenamiento de claves). Las propiedades expect/actual (val) son adecuadas para constantes de plataforma — nombre del SO, versión del SDK o ruta del directorio del sistema.
| Tipo de declaración | Palabras clave | Ejemplo de uso |
|---|---|---|
| Función | expect fun / actual fun | Obtener un identificador único de dispositivo |
| Clase | expect class / actual class | Acceder a SecureStorage (Keychain / EncryptedSharedPreferences) |
| Propiedad | expect val / actual val | Plataforma actual (iOS / Android) |
| Enum class | expect enum / actual enum | Lista de permisos de aplicación disponibles |
| Typealias | expect typealias / actual typealias | Tipo de respuesta de red específico de la plataforma |
No todas las construcciones de Kotlin se pueden usar con expect/actual. Una declaración expect no puede contener un cuerpo — solo una firma. Una clase expect no puede tener un constructor con parámetros (debe tener un constructor primario vacío). Para enum expect/actual, todas las constantes deben ser idénticas tanto en expect como en actual. Las propiedades expect deben ser val (no var), ya que almacenar estado en el módulo común para propiedades de plataforma no tiene sentido.
Exploremos ejemplos prácticos de expect/actual desde funciones simples hasta clases completas. El caso básico es obtener el nombre de la plataforma para usarlo en la interfaz de usuario. Los ejemplos más complejos incluyen el acceso al almacenamiento nativo y el trabajo con hilos de plataforma.
// 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() }
}
En este ejemplo, la clase expect PlatformStorage define el contrato de un almacenamiento simple clave-valor. En Android, la implementación usa SharedPreferences, mientras que en iOS usa Keychain o NSUserDefaults. Gracias a expect/actual, la lógica de negocio en commonMain llama a save/get/remove sin conocer la implementación de la plataforma.
// 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)
}
}
Al diseñar API expect/actual, se deben seguir varios principios. Minimice la cantidad de declaraciones expect — cuanto más código común, más sencillo es el mantenimiento. Use expect/actual solo para API que realmente difieren entre plataformas. Para el resto del código, use interfaces con fábricas o inyección de dependencias, lo que simplifica las pruebas.
Se recomienda agrupar las declaraciones expect por módulos temáticos, en lugar de mezclarlas en un solo archivo. Por ejemplo, Storage.kt para declaraciones expect de almacenamiento, Platform.kt para funciones expect que trabajan con el SO y Analytics.kt para clases expect de analítica. Esto simplifica la navegación y la comprensión de la superficie de plataforma de un proyecto KMM. Cada archivo actual debe estar en el source set correspondiente: androidMain, iosMain, desktopMain, etc.
Las implementaciones predeterminadas mediante expect fun con actual fun donde actual usa código común es un antipatrón común. Si la implementación de la plataforma no difiere de la predeterminada, expect/actual no es necesario. En tales casos, use una función simple en commonMain. Evite también expect/actual para getters triviales — use expect val con constantes.
La estructura adecuada del código expect/actual es crítica para la legibilidad del proyecto. Cada módulo expect/actual debe tener un único punto de entrada. Ejemplo de organización: commonMain/kotlin/com/project/platform contiene las declaraciones expect, androidMain/kotlin/com/project/platform contiene actual para Android, iosMain/kotlin/com/project/platform contiene actual para iOS. Los nombres de archivos y paquetes deben coincidir para expect y actual, de modo que un desarrollador pueda encontrar rápidamente la implementación correspondiente.
Las interfaces con una fábrica de plataforma son la principal alternativa a expect/actual. En lugar de una clase expect, puede declarar una interfaz en commonMain y crear clases concretas en los módulos de plataforma. Una fábrica o un contenedor de inyección de dependencias proporciona la implementación correcta en tiempo de ejecución. Este enfoque es mejor para pruebas, ya que la interfaz se puede simular.
La inyección de dependencias (Koin, Kodein) es un enfoque más flexible pero menos eficiente. Un contenedor DI se configura por separado para cada plataforma y proporciona dependencias de plataforma al código común. A diferencia de expect/actual, la inyección ocurre en tiempo de ejecución, lo que permite intercambiar implementaciones para pruebas. Por otro lado, los errores de configuración de DI solo se detectan en tiempo de ejecución, no en tiempo de compilación.
| Enfoque | Verificación en compilación | Flexibilidad de pruebas | Sobrecarga en ejecución |
|---|---|---|---|
| expect/actual | Completa | Baja (actual no se puede simular) | Cero (enlace en compilación) |
| Interfaces + Fábrica | Parcial | Alta (se puede simular) | Mínima (llamada virtual) |
| Inyección de dependencias | No (runtime) | Alta | Moderada (proxies DI) |
La elección entre expect/actual y alternativas depende del contexto. Para código crítico en rendimiento (motores de juegos, procesamiento en tiempo real), expect/actual es preferible por su sobrecarga cero. Para lógica de negocio (repositorios, casos de uso), es mejor usar interfaces con DI para simplificar las pruebas. Un enfoque combinado — expect/actual para operaciones de bajo nivel de plataforma e interfaces para la capa de lógica de negocio — se utiliza en la mayoría de los proyectos KMM de producción.
Preguntas frecuentes
expect/actual vincula la implementación en tiempo de compilación sin llamadas virtuales, mientras que las interfaces lo hacen en tiempo de ejecución. expect/actual garantiza la implementación para todas las plataformas, las interfaces requieren verificaciones en tiempo de ejecución.
Sí, expect enum es compatible desde Kotlin 1.7. Todas las constantes en los enums expect y actual deben coincidir. Diferentes valores de constantes en diferentes plataformas es un error de compilación.
El compilador generará un error para cada plataforma donde falte la implementación actual. El proyecto no se compilará hasta que se añadan las implementaciones actual correspondientes para todas las declaraciones expect.
No, expect y actual deben estar en diferentes source sets. expect en commonMain o un source set intermedio, actual en un source set de plataforma. Colocar expect y actual en el mismo source set es un error de compilación.
Para probar expect/actual, use commonTest con source sets de prueba de plataforma. Escriba pruebas expect en commonTest y pruebas actual para cada plataforma. Las pruebas de integración se ejecutan por separado en cada plataforma objetivo.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también