En el artículo anterior de la serie (Unidad 1 — Tema 3), desglosamos el esqueleto físico del monorepo fullstack y organizamos la estructura modular de directorios. Con las carpetas en su sitio, el gran reto es lograr que el código compartido dialogue fluidamente con el hardware e infraestructura de cada sistema operativo sin pagar penalizaciones de rendimiento.
En el desarrollo multiplataforma, el Santo Grial es maximizar la reutilización de código sin perder el acceso a las capacidades nativas de cada sistema operativo. Kotlin Multiplatform (KMP) aborda este desafío de una manera radicalmente distinta a otros frameworks que introducen capas de abstracción pesadas o puentes de serialización en tiempo de ejecución. KMP prefiere resolver la interoperabilidad en tiempo de compilación.
Cuando el compilador de Kotlin genera los binarios para Android (JVM) e iOS (Apple Silicon/Intel), necesita un mecanismo seguro para enlazar el código común con las APIs nativas de cada plataforma. Aquí es donde entra el mecanismo de expect/actual. No se trata de un simple reflejo o una inyección de dependencias tradicional en tiempo de ejecución; es un contrato estricto a nivel de compilador que nos permite declarar «expectativas» en nuestro código compartido y exigir «realidades» en el código de la plataforma. En este artículo, analizaremos a fondo cómo funciona este ciclo, sus reglas a nivel de arquitectura y cómo implementarlo de forma eficiente.
1. El Mecanismo Expect/Actual a Nivel de Compilador
A diferencia de los patrones de diseño tradicionales donde las abstracciones se resuelven dinámicamente, el par expect/actual funciona como un enlace en tiempo de compilación. Cuando declaras un elemento con la palabra clave expect en el source set commonMain, estás definiendo un contrato topológico. El compilador de Kotlin no generará el artefacto final a menos que cada una de las plataformas destino (como androidMain e iosMain) provea una implementación idéntica marcada con actual.
Es importante entender que expect no es una interfaz. Mientras que una interfaz define un comportamiento que una clase debe implementar en tiempo de ejecución, expect puede aplicarse a clases, funciones, propiedades, enumerados o funciones de extensión. El compilador valida que las firmas (nombre, parámetros, tipos de retorno y modificadores de visibilidad) sean compatibles entre la declaración expect y las declaraciones actual. Si hay una discrepancia no permitida, la compilación fallará antes de generar cualquier binario.
2. Reglas Estrictas y Restricciones de Visibilidad
Para dominar este mecanismo, es crucial comprender las restricciones que impone el compilador de Kotlin para garantizar la seguridad de tipos:
- Coincidencia de Firmas: Los nombres de los parámetros y sus tipos deben ser exactamente los mismos. No se permite cambiar el nombre de un argumento en la implementación
actual, ya que esto rompería la resolución de argumentos con nombre (named arguments) en el código común. - Modificadores de Visibilidad: La declaración
actualdebe tener una visibilidad compatible con laexpect. En Kotlin actual, eso significa que puede mantener la misma visibilidad o incluso ser más permisiva que el contrato esperado; lo que no puede hacer es ser más restrictiva y dejar inaccesible algo que el código común esperaba usar. - Restricción de Paquetes: Tanto la declaración
expectcomo laactualdeben residir exactamente en el mismo paquete (package) dentro de sus respectivos source sets. El compilador utiliza el nombre completamente cualificado (FQCN) para emparejarlos durante la fase de enlace.
3. Ejemplo Práctico: Un Identificador Único de Dispositivo
Veamos un ejemplo genérico pero de arquitectura sénior: obtener un identificador único de plataforma o el nombre del sistema operativo. Esto requiere acceder a las APIs nativas de Android e iOS, pero queremos exponer una propiedad limpia en nuestro dominio compartido.
En commonMain (El Contrato):
package dev.asiles.blog.core
/**
* Define el contrato para obtener información del entorno nativo.
* El compilador garantizará que cada plataforma resuelva esta propiedad.
*/
public expect class PlatformInfo() {
public val operatingSystemName: String
public fun logDebugInfo(): Unit
}
En androidMain (La Realidad en Android):
package dev.asiles.blog.core
import android.os.Build
public actual class PlatformInfo {
// Usamos la API nativa de Android (SDK de Java/Android)
public actual val operatingSystemName: String = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
public actual fun logDebugInfo() {
android.util.Log.d("PlatformInfo", "Running on $operatingSystemName")
}
}
En iosMain (La Realidad en iOS):
package dev.asiles.blog.core
import platform.UIKit.UIDevice
public actual class PlatformInfo {
// Acceso directo a las APIs de Apple a través del interop nativo de Kotlin/Native
public actual val operatingSystemName: String = "${UIDevice.currentDevice.systemName} ${UIDevice.currentDevice.systemVersion}"
public actual fun logDebugInfo() {
// En iOS usamos la función de impresión estándar de Kotlin/Native que mapea a la consola nativa
println("PlatformInfo: Running on $operatingSystemName")
}
}
4. Enfoque Arquitectónico: Cuándo Usarlo y Cuándo Evitarlo
El mecanismo expect/actual es extremadamente potente, pero en la arquitectura de un sistema de nivel empresarial, debe ser tu última opción, no la primera.
Cuándo usarlo:
- Para construir wrappers muy delgados sobre tipos primitivos o firmas de funciones que dependen directamente de tipos de la plataforma (por ejemplo, dar formato a una fecha, interactuar con tipos atómicos o inicializar una base de datos local como SQLDelight).
- Cuando necesitas instanciar una clase nativa cuyo constructor requiere un contexto específico de la plataforma.
Cuándo evitarlo (y qué usar en su lugar):
- No lo uses para lógica de negocio pesada. Si creas grandes clases
expect, estarás duplicando lógica y rompiendo el propósito de KMP. - Alternativa superior (Inyección de Dependencias / Inversión de Control): Para componentes arquitectónicos complejos (como clientes HTTP personalizados, gestores de Bluetooth o sistemas de almacenamiento), es mucho más limpio definir una interfaz estándar de Kotlin (
interface) encommonMaine inyectar las implementaciones nativas mediante un framework de DI como Koin o mediante inyección manual en el punto de entrada de la aplicación. Esto facilita los tests unitarios encommonMainmediante el uso de dobles de prueba (mocks/fakes) sin depender del mecanismo de compilación.
5. Type Aliasing: El Truco Avanzado de Rendimiento
Existe una característica avanzada dentro del ecosistema actual que los desarrolladores senior deben aprovechar: los actual typealias. Si una plataforma ya tiene un objeto que cumple perfectamente con el contrato que necesitas, no es necesario crear una clase envoltorio (wrapper) que añada sobrecarga en tiempo de ejecución. Puedes simplemente mapearlo.
Por ejemplo, si necesitas un tipo de dato para manejar UUIDs de forma nativa, puedes declarar un expect class UUID en el código común. En androidMain, en lugar de reimplementar la clase, puedes hacer:
// En androidMain
public actual typealias UUID = java.util.UUID
Esto le dice al compilador que, cuando compile para Android, reemplace directamente cualquier uso de tu UUID compartido por la clase nativa optimizada de Java, eliminando cualquier capa de abstracción intermitente y manteniendo el rendimiento nativo al 100%.
Para profundizar
Para contrastar estos conceptos arquitectónicos con las especificaciones técnicas vigentes, recomiendo encarecially la lectura de los siguientes recursos de la documentación oficial de JetBrains:
- Connect to platform-specific APIs (Official Kotlin Documentation): La guía oficial sobre las reglas de diseño y casos de uso del mecanismo expect/actual.
- Interoperability with Swift/Objective-C: Fundamental para entender cómo se mapean los tipos primitivos y complejos cuando compilas el código compartido hacia ecosistemas Apple.
Conclusión
El mecanismo expect/actual es uno de los pilares que hacen de Kotlin Multiplatform una tecnología tan robusta y respetuosa con el rendimiento nativo. Al delegar la resolución de contratos al tiempo de compilación en lugar de pagar el costo de rendimiento en tiempo de ejecución, KMP nos permite estructurar aplicaciones altamente optimizadas.
Como buena práctica senior, mantén tus declaraciones expect lo más minimalistas posible. Trátalas como los puntos de anclaje de tu infraestructura y construye el resto de tu arquitectura (interfaces, casos de uso y ViewModels) sobre abstracciones puras en commonMain. Esto garantizará que tu base de código sea escalable, fácil de testear y preparada para añadir nuevas plataformas en el futuro con un esfuerzo mínimo.
Una vez consolidado el plano teórico y las reglas de visibilidad del compilador, es hora de poner en práctica este mecanismo en un escenario real de producción. En el próximo artículo, Unidad 2 — Tema 2: Escribiendo código específico de plataforma: Obteniendo información del dispositivo en iOS y Android, nos arremangaremos para implementar un lector de telemetría de hardware conectando con las APIs nativas de Android SDK y CocoaTouch.







