Etiqueta: Source Sets

  • Unidad 1 — Tema 3: Estructura de un proyecto KMP: ¿Dónde vive mi código compartido?

    Unidad 1 — Tema 3: Estructura de un proyecto KMP: ¿Dónde vive mi código compartido?

    En el artículo anterior de la serie (Unidad 1 — Tema 2), pusimos a punto nuestro entorno de desarrollo y logramos tener listos Xcode, Android Studio y KDoctor para compilar en cualquier plataforma. Con la máquina ya configurada, es hora de dar el primer paso práctico: crear nuestro proyecto. Al abrir el repositorio multiplataforma por primera vez, es normal sentir cierto desconcierto.

    Si vienes del desarrollo web fullstack o de entornos backend distribuidos, estás acostumbrado a mitigar un problema histórico: la sincronización de contratos. Mantener los modelos de datos de tu API en TypeScript mapeados idénticamente con tus entidades del backend en Java, C# o Go suele requerir generadores de código o un mantenimiento manual propenso a fallos. Al aterrizar en un proyecto Kotlin Multiplatform (KMP) fullstack, la primera fricción mental desaparece al abrir el repositorio: no estamos ante una simple app móvil, sino ante un monorepo Gradle nativo que orquesta el backend, el navegador y los clientes nativos bajo una única fuente de verdad.

    El error más común al empezar con KMP es heredar los vicios de las arquitecturas móviles simples. Cuando el Kotlin Multiplatform Wizard oficial genera un proyecto con soporte para Servidor, Web, Desktop y Móvil, estructura el árbol de directorios de forma modular para escalar limpiamente. En este artículo vamos a destripar un ejemplo representativo de proyecto fullstack, entendiendo la separación de responsabilidades entre submódulos y cómo las entrañas de Gradle configuran el grafo de dependencias.

    1. El Grafo de SourceSets: La jerarquía del código multiplataforma

    A diferencia de un proyecto de software tradicional con su clásico src/main/kotlin, KMP introduce el concepto de SourceSets. Un SourceSet es una agrupación de código fuente, recursos y dependencias que apunta a un objetivo de compilación específico o a una abstracción intermedia en el árbol de compilación.

    El motor que orquesta esta magia es la configuración de estructura jerárquica (Hierarchical Structure Support) introducida por el plugin multiplataforma de Kotlin. En la cúspide de esta pirámide se encuentra el denominador común: commonMain.

    • commonMain: Es el núcleo agnóstico al entorno de ejecución. El código aquí escrito se compila usando el compilador común de Kotlin y solo puede acceder a la librería estándar y a dependencias multiplataforma puras (Ktor, kotlinx.serialization). No hay acceso a java.*, javax.*, ni a las APIs nativas del navegador o de iOS.
    • Plataformización dirigida: A medida que descendemos en el grafo, el compilador especializa el entorno. En los targets basados en la JVM (como el servidor o el escritorio), tienes acceso total al ecosistema Java. En los targets de Apple, el compilador Kotlin/Native expone los frameworks del sistema (Foundation, UIKit). Y en el target Web, el compilador genera JavaScript o WebAssembly (Wasm), dándote acceso directo al DOM de la página.

    2. Anatomía de la raíz: Desglosando el Monorepo Fullstack

    Al configurar un proyecto KMP que abarca todo el stack tecnológico, el Wizard organiza el código aislando los entornos de ejecución clientes dentro de un directorio paraguas y segregando la lógica pura. La estructura exacta puede variar según la versión del wizard y las opciones marcadas, así que el siguiente árbol debe leerse como un ejemplo posible y realista, no como una salida inmutable o literal de todas las versiones:

    ├─ app/                  # Capa de Presentación de los Clientes (Frontend)
    │   ├─ androidApp/       # Módulo Gradle: Punto de entrada para Android
    │   ├─ desktopApp/       # Módulo Gradle: Punto de entrada para Escritorio (JVM)
    │   ├─ iosApp/           # Proyecto Xcode nativo (Swift / SwiftUI)
    │   ├─ webApp/           # Módulo Gradle: Configuración para Kotlin/Wasm o Kotlin/JS
    │   └─ shared/           # UI Compartida (Compose Multiplatform) para todos los clientes
    ├─ core/                 # Lógica de negocio pura compartida entre CLIENTE y SERVIDOR
    ├─ server/               # Backend de la aplicación (Ktor / Entorno JVM)
    └─ gradle/
        └─ libs.versions.toml # Catálogo centralizado de dependencias (Version Catalog)

    El contenedor app/ y la UI compartida

    Las aplicaciones androidApp, desktopApp, iosApp y webApp suelen actuar como cascarones de inicialización. En una variante basada en Compose Multiplatform, la lógica de UI real (pantallas, componentes visuales, estados de vista) puede residir en app/shared/src/commonMain. Pero esa no es la única opción: otros proyectos mantienen toda la UI nativa en cada cliente y comparten solo datos y dominio. Lo importante es entender la responsabilidad de cada módulo, no memorizar un árbol cerrado.

    El módulo core/ (La frontera Fullstack)

    Este es el componente más crítico del sistema. Es un módulo KMP puro (sin dependencias de interfaz de usuario). Su único objetivo es albergar los modelos de datos (DTOs), reglas de validación comunes y configuraciones de red que consumirá tanto la UI en el cliente como el Backend en el servidor.

    3. Configuración del sistema de módulos en Gradle

    Para que este monorepo funcione sin acoplamientos cíclicos, la orquestación en los archivos de configuración de Gradle debe ser limpia y rigurosa. Todo comienza en la raíz del proyecto, donde le indicamos al motor de construcción la estructura modular.

    A nivel de código práctico, la vinculación jerárquica se declara en el archivo settings.gradle.kts:

    // settings.gradle.kts (Raíz del proyecto)
    rootProject.name = "Demo"
    
    // Módulos base independientes
    include(":core")
    include(":server")
    
    // Módulos anidados bajo el contenedor de clientes (app)
    include(":app:shared")
    include(":app:androidApp")
    include(":app:desktopApp")
    include(":app:webApp")
    

    La verdadera ventaja competitiva de este diseño se aprecia en cómo el módulo server consume al módulo core como una dependencia local interna. Al ser un entorno backend (por ejemplo, usando Ktor), su archivo build.gradle.kts se simplifica drásticamente:

    // server/build.gradle.kts
    plugins {
        alias(libs.plugins.kotlinJvm)
        alias(libs.plugins.ktor) // Plugin oficial de Ktor Server
    }
    
    dependencies {
        // Inyección directa de la lógica de negocio y contratos comunes
        api(projects.core)
        
        // Infraestructura del servidor
        implementation(libs.logback)
        implementation(libs.ktor.serverCore)
        implementation(libs.ktor.serverNetty) // Motor de ejecución embebido
    }

    4. Enfoque Arquitectónico Sénior: El Flujo de Datos Unificado

    Diseñar un monorepo fullstack con KMP introduce ventajas operativas enormes, pero exige disciplina arquitectónica para no convertir el proyecto en un monolito acoplado indescifrable.

    El flujo ideal del dato Compartido

    1. Una entidad de negocio UserSession se define en el módulo :core.
    2. El módulo :server procesa la autenticación, serializa UserSession a JSON y la expone en un endpoint.
    3. El módulo :app:shared (Frontend) realiza la petición HTTP a través de Ktor Client, recibe el JSON y, al tener :core mapeado como dependencia, deserializa automáticamente el flujo a la clase exacta UserSession. No hay mapeos intermedios ni archivos autogenerados en TypeScript.

    Cuándo evitar la compartición absoluta y usar aislamiento

    • Arquitecturas Backend complejas: Si tu servidor necesita conectarse a bases de datos relacionales vía Hibernate/Exposed o interactuar con colas de mensajería (Kafka, RabbitMQ), ese código jamás debe rozar el módulo :core. El módulo :core debe permanecer estéril, limitado a estructuras de datos puras y lógica computacional agnóstica.
    • Alternativas en Frontend: Si el rendimiento visual en la web es crítico y necesitas SEO indexable por motores de búsqueda tradicionales, forzar Compose Multiplatform vía WebAssembly en app/webApp puede ser contraproducente, ya que renderiza la UI en un <canvas>. Una alternativa senior es usar KMP en la web solo para la capa de datos (:core) y acoplarla a un framework JS nativo mediante Kotlin/JS.

    5. El proceso de entrega: Compilación multiplataforma en tiempo de ejecución

    Tener todos los módulos bajo el mismo paraguas de Gradle no significa que se compilen en un único ejecutable. El flujo de compilación segmenta los artefactos según sus naturalezas nativas:

    • Backend (:server): El compilador ejecuta kotlinJvm. Compila el código del servidor y el de :core en archivos .class estándar de Java, listos para ser empaquetados en un Fat JAR ejecutable sobre cualquier JRE 17 o superior.
    • Navegador (:app:webApp): El compilador traduce el código Kotlin a binarios de WebAssembly (.wasm) y genera un pegamento mínimo en JavaScript (.js) junto al archivo HTML index, optimizando el tamaño del bundle mediante el DCE (Dead Code Elimination).
    • Móviles (:app:androidApp y :app:iosApp): Android genera su bytecode intermedio optimizado por R8/D8, mientras que el pipeline de iOS genera el framework binario de Kotlin/Native que después enlaza Xcode. La herramienta cinterop no se usa para «encapsular» tu framework exportado, sino para generar bindings de Kotlin a librerías nativas externas cuando necesitas consumir APIs de C u Objective-C desde el código Kotlin.

    Para profundizar

    Para estudiar a fondo el comportamiento de los entornos de ejecución web, la integración de redes y la estructura modular oficial del ecosistema, consulta los portales de referencia:

    Conclusión

    El Wizard fullstack de Kotlin Multiplatform nos demuestra que el lenguaje ha madurado hacia una solución de ingeniería de software global. Al separar radicalmente la lógica de presentación de los clientes de las reglas e intercambio de datos del negocio, se consigue un desacoplamiento que respira las mejores prácticas de la arquitectura limpia. Mantener el módulo core libre de dependencias de plataforma es la mayor garantía de rendimiento y mantenibilidad que un arquitecto de software puede asegurar en este ecosistema.

    En el próximo artículo, Unidad 2 — Tema 1: Interoperabilidad Nativa: Cómo usar expect/actual en KMP, daremos el salto a la acción escribiendo nuestro primer código multiplataforma y aprendiendo cómo invocar de manera segura APIs del sistema nativo de Android e iOS utilizando las palabras clave expect y actual.