Kotlin/Native y Multiplatform Parte 1

16 minute read

En esta primer parte de la publicación, aprenderemos un poco de Kotlin/Native y crearemos una aplicación basica multiplataforma de Kotlin que se ejecuta tanto en iOS como en Android utilizando código Kotlin compartido.

Que es Kotlin/Nativo

Kotlin comenzó su existencia centrado en la JVM sin embargo ha ampliado su alcance para incluir el desarrollo nativo y web. Kotlin/Native permite compilar código Kotlin fuera de las máquinas virtuales, lo que da como resultado binarios autónomos que son nativos del entorno en el que se ejecutan como se muestra en el siguiente diagrama

GitHub konan

El compilador de Kotlin/Native se llama Konan y este maneja el front-end del proceso, compilando el código fuente de Kotlin en una Representación Intermedia (IR) que es la entrada al back-end del proceso. Kotlin/Native aprovecha el compilador LLVM para el back-end. Al combinar Konan con LLVM, Kotlin/Native permite producir ejecutables nativos a partir de su código Kotlin. En teoría, Kotlin/Native se puede utilizar en cualquier plataforma compatible con LLVM.

Instalación

El compilador Kotlin/Native está disponible para macOS, Linux y Windows. Está disponible como una herramienta de línea de comandos, se puede descargar desde el sitio web de kotlin. Si bien la salida del compilador no tiene dependencias ni requisitos de máquina virtual, el propio compilador requiere Java 1.8 o superior

GitHub asset

Una vez descargado descomprimimos el archivo

$ tar -xzvf kotlin-native-macos-1.3.50.tar.gz

Creando Hello Kotlin/Native

La aplicación imprimirá “Hello Kotlin/Native” en la salida estándar. En un directorio de trabajo de nuestra elección, crearemos un archivo con el nombre hello.kte ingresamos el siguiente contenido:

fun main() {
  println("Hello Kotlin/Native!")
}

Para compilar la aplicación, usaremos el siguiente comando:

$ konanc -o hello hello.kt

El valor de -o especifica el nombre del archivo de salida, por lo que esta llamada debe generar un archivo binario hello.kexe(Linux y macOS) o hello.exe(Windows)

Si bien la compilación desde la consola parece ser fácil y clara, no se adapta bien a proyectos más grandes con cientos de archivos y bibliotecas. Para proyectos del mundo real, se recomienda utilizar un sistema de compilación y un IDE .

Kotlin Multiplataforma

Ahora comenzaremos el desarrollo de aplicaciones multiplataforma. Queremos crear versiones de la aplicación para iOS y Android, y queremos que las dos aplicaciones tengan la misma funcionalidad, hacer la llamada de red, analizar los resultados y mostrar una lista. En comparación con muchas aplicaciones de producción, nuestra proyecto será bastante básico. Sin embargo, está realizando diversas tareas típicas: redes, deserialización de JSON y pasar los resultados a las vistas para su visualización.

Si creamos la aplicación para iOS y la aplicación para Android por separado, habría una gran cantidad de duplicación de código entre las dos aplicaciones:

GitHub tipical-app-dev

Eso es mucha duplicación incluso para una aplicación simple. Imaginemos que hubiera más pantallas en nuestra aplicación, con más datos, llamadas de red y almacenamiento en caché local de los datos remotos, como lo habría en una aplicación con más funciones.

La cantidad de código duplicado aumentaría linealmente con el tamaño de la aplicación, al igual que la cantidad de tiempo y esfuerzo para producir primero y luego mantener las dos aplicaciones.

Reducir esta duplicación tanto en iOS como en Android ha sido durante mucho tiempo una visión de muchos desarrolladores y organizaciones en el mundo móvil.

GitHub cross-platform-frameworks

Estos y otros conjuntos de herramientas multiplataforma han sido muy prometedores, pero ninguno se ha arraigado realmente en el mundo del desarrollo móvil. Hay muchas razones para esto, algunas técnicas, otras menos técnicas. Algunas de las razones son:

  • El bajo rendimiento de las aplicaciones resultantes
  • Las inconsistencias con las interfaces de usuario nativas
  • La incapacidad de mantenerse actualizado con las últimas funciones de iOS y Android
  • La lealtad y la experiencia de los desarrolladores con los SDK
    nativos.

Aquí es donde entra en juego Kotlin Multiplatform. No es un marco multiplataforma. Es más un enfoque para el desarrollo de aplicaciones móviles que de alguna manera le brinda lo mejor de todos los mundos posibles. Con Kotlin Multiplatform, podemos reducir la duplicación de código poniendo código común a todas las aplicaciones de front-end en un módulo o proyecto compartido. Esto incluye lógica de negocios y cosas como código de networking, análisis de datos, persistencia de datos y más. Podemos usar varios patrones arquitectónicos y, en una aplicación grande, podría considerar algo como Arquitectura limpia, donde todas las capas internas del software se comparten entre los front-end, y solo la capa más externa es única para una plataforma determinada como iOS. o Android. Esto reduce significativamente la cantidad de duplicación en el software, ya que la mayor parte o toda la lógica y la funcionalidad están escritas en un solo lugar.

En este proyecto no construiremos una arquitectura completamente limpia, pero usaremos una estructura MVP simple, donde el modelo y el código de presentación se comparten entre las plataformas, junto con las definiciones de la interfaz de vista. El código de la aplicación para iOS y Android implementará la interfaz de visualización e interactuará con los presenters en el módulo compartido.

GitHub mvp

Más allá de reducir la duplicación de código, Kotlin Multiplatform ofrece otros beneficios como:

  • Interfaces de usuario creadas con código nativo.
  • Especialmente en un proyecto de desarrollo de aplicaciones más grande, se puede dividir el equipo en grupos que trabajan en diferentes áreas. Se puede tener un grupo dedicado al código compartido, un grupo dedicado a la interfaz de usuario de Android y un grupo dedicado a la interfaz de usuario de iOS.
  • Se puede escribir el código de la interfaz de usuario de iOS en Kotlin en lugar de Swift.

Creación de proyecto Multiplataforma

Aquí hay un vistazo de cómo se verá la estructura de carpetas de nuestro proyecto Kotlin Multiplatform cuando terminemos. Tenemos una carpeta androidApp para el código de interfaz de usuario específico de Android, una carpeta iosApp para el código de interfaz de usuario específico de iOS y una carpeta compartida donde irá el código Kotlin compartido. En la raíz del proyecto, hay un archivo llamado build.gradle, que es un archivo de compilación común a todo el proyecto. También hay archivos build.gradle en la carpeta compartida. La carpeta iOS tiene el típico archivo de proyecto Xcode para el proyecto de la aplicación iOS. El sistema de compilación Gradle utiliza los archivos build.gradle para administrar la compilación y definir dependencias en cada parte del proyecto

GitHub multiplatform-project

Para la creación de nuestro proyecto vamos a usar Android Studio. Estoy usando Android Studio 4.0.1 con SDK 28 o posterior instalado y el complemento Kotlin 1.3.72.

  1. Crear un nuevo proyecto de tipo Empty Activity y presionamos siguiente.
  2. El nombre del proyecto sera GitHubKMP.
  3. El nombre del paquete es mobile.world.githubkmp
  4. El lenguaje es Kotlin.
  5. Elegimos una ubicación
  6. Seleccionamos el API 21 para el nivel mínimo de SDK de Android admitido y nos aseguramos de que AndroidX esté marcado en caso de estar disponible.
  7. Luego, hago clic en Finalizar.

GitHub new-project

El proyecto se abrirá y se ejecutará una compilación de Gradle. Cuando está termine, actualizamos las versiones de dependencia del archivo build.gradle de la aplicación de Android para asegurarnos de tener las últimas dependencias de AndroidX. Y luego presioné el botón sincronizar y ejecuto la aplicación de Android en el emulador.

GitHub update-versions

Queremos que el nombre de la carpeta de la aplicación de Android sea androidApp en lugar de solo la app predeterminada para distinguir esa parte del proyecto como la aplicación de Android. Para cambiar el nombre de la carpeta, primero cierro el proyecto.

En la ventana de Terminal, uso move para cambiar el nombre de la carpeta de app a androidApp

$ mv app androidApp

En el archivo settings.gradle para el proyecto, actualizo el include para usar androidApp en lugar de app.

$ vim settings.gradle

Después de guardar el cambio, abrimos el proyecto y lo limpiamos desde el menu Buid -> Clean Project

Así es como se verá la estructura de carpetas del proyecto compartido cuando hayamos terminado.

GitHub update-project.gradle

Shared Project

GitHub shared

Como vemos en la imagen, hay una carpeta commonMain en la que colocaremos el código que es común a todas las partes de la aplicación. En GitHubKMP, ese código incluirá networking, deserialización JSON de objetos de modelo, presenters y la definición de interfaces de vista, algunos de los cuales veremos el la parte dos de este artículo. También se incluyen las carpetas androidMain e iosMain para la implementación de código compartido de cada plataforma. Y por último también el build.gradle compartido.

Vamos a construir este proyecto compartido más o menos a mano. De esa manera, veremos todo lo que se necesita para crear el proyecto compartido. Es posible utilizar IDE como AppCode para automatizar algunos de estos pasos.

Desde la raíz del proyecto multiplataforma, primero crearemos los directorios commonMain, androidMain, iosMain y main.

$ mkdir -p shared/src/commonMain/kotlin
$ mkdir -p shared/src/androidMain/kotlin
$ mkdir -p shared/src/iosMain/kotlin
$ mkdir -p shared/src/main

A continuación, agregamos los archivos fuente de Kotlin con los que comenzaremos, junto con un archivo AndroidManifest.xml en la carpeta principal y un archivo build.gradle para el proyecto compartido. Necesitamos el archivo AndroidManifest ya que el código compartido se compilará en una biblioteca de Android en la que podremos acceder al SDK de Android. Si no necesitamos acceso al SDK de Android desde el código compartido, no es necesario crear el archivo manifest.

$ touch shared/src/commonMain/kotlin/common.kt
$ touch shared/src/androidMain/kotlin/android.kt
$ touch shared/src/iosMain/kotlin/ios.kt
$ touch shared/src/main/AndroidManifest.xml 
$ touch shared/build.gradle

Volviendo a Android Studio, cambiamos el panel del proyecto de Android a la vista del proyecto para ver la estructura de carpetas y archivos del proyecto, incluidas las carpetas y los archivos que acabamos de agregar.

GitHub project

A continuación, haremos algunas adiciones al archivo build.gradle a nivel de proyecto. Los archivos build.gradle generalmente se escriben en el lenguaje de programación Groovy, que es un lenguaje JVM alternativo como el propio Kotlin al principio, así como en lenguajes como Scala y Clojure. Recientemente, ha sido posible escribir los archivos build.gradle en Kotlin, pero nos quedaremos con Groovy, ya que es la forma predeterminada y la forma en que los desarrolladores de Android están familiarizados. En el futuro, es muy posible que Kotlin se convierta en el idioma predeterminado para los archivos de compilación.

En el archivo build.gradle del proyecto raíz

  • Agregamos la versión Kotlin/Native que usaremos:
    ext.kotlin_native_version = "1.3.72"
    
  • Luego agregaremos algunos repositorios de Maven
    maven { url 'https://plugins.gradle.org/m2/' }
    maven { url 'http://dl.bintray.com/kotlin/kotlin-eap' }
    maven { url 'https://dl.bintray.com/jetbrains/kotlin-native-dependencies' }
    
  • Agregamos la ruta de clase del complemento Kotlin/Native Gradle, que extrae ese complemento en Android Studio.
    classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"
    

GitHub update-gradle

Android Studio nos pedirá que realicemos una sincronización del proyecto para los cambios del archivo gradle. Cuando se complete la sincronización, para asegurarnos que todo sigue funcionando, ejecutamos la app.

Incluir modulo shared en el archivo settings.gradle

include ':shared'

Configurar el archivo shared/buid.gradle

apply plugin: 'com.android.library'
apply plugin: 'kotlin-multiplatform'

android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 15
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
kotlin {
    targets {
        final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \
                                 ? presets.iosArm64 : presets.iosX64
        fromPreset(iOSTarget, 'ios') {
            binaries {
                framework('shared')
            }
        }
        fromPreset(presets.android, 'android')
    }
    sourceSets {
        commonMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib-common'
        }
        androidMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib'
        }
        iosMain.dependencies {
        }
    }
}
configurations {
    compileClasspath
}

En el archivo AndroidManifest.xml agregamos el sigueinte codigo:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="mobile.world"/><code>

Configuramos el atributo del paquete para que coincida con el paquete que se utilizará para el código compartido.

Expect y actual keywords

Compilar el proyecto compartido para todas las plataformas no es el enfoque adoptado por Kotlin/Multiplatform. En cambio, una cierta cantidad de código es común a todas las plataformas, pero otra parte del código es única para cada plataforma.

El código androidMain se compilará usando kotlin-jvm, y el código iOSMain será compilado por Kotlin/Native. Cada uno se combinará con la versión compilada del código común para las respectivas plataformas.

El mecanismo expect/actual ha sido agregado a kotlin. La palabra expect nos servirá para definir una interfaz que se pueda utilizar en el código compartido como una interfaz en Kotlin o un protocolo en Swift, y actual para proporcionar implementaciones dependientes de cada plataforma.

En nuestro primer ejemplo de expect y actual, vamos a esperar que cada plataforma pueda decirle al código compartido cuál es su nombre como un String usando la función platformName. Luego usaremos la función platformName en otras partes del código compartido, como se muestra aquí en la función Greeting.

package world.mobile

expect fun platformName(): String

class Greeting {
    fun greeting(): String = "Hello, ${platformName()}"
}

GitHub expects

Expect se puede usar en funciones, clases o propiedades, sin embargo todo lo marcado con expect no puede tener implementaciones . Por lo tanto, no puede definir una función dentro de una clase marcada con la palabra clave expect pero podemos usar esas declaraciones como si estuvieran definidas.

En iOSMain y androidMain, necesitamos la funcion platformName de tipo actual

En shared/src/androidMain/kotlin/android.kt, agregamos una versión real de platformName para Android. Observe el uso de la clase android.os.Build

package mobile.world
import android.os.Build

actual fun platformName(): String {
    return "Android ${Build.VERSION.RELEASE}"
}

En shared/src/ios/kotlin/ios.kt, agrego una versión real de platformName para iOS. Observe la importación del paquete de plataforma de la clase UIKit

package mobile.world
import platform.UIKit.UIDevice

actual fun platformName(): String {
    return "${UIDevice.currentDevice.systemName}"
}

GitHub expects

Si hago clic en la E, nos llevan a la definición expect correspondiente y al hacer clic en la A, pudemos navegar a cualquiera de las implementaciones actual.

GitHub def_expect GitHub implements

Ahora tenemos un código compartido que podemos construir. Hay dos formas que usaremos para hacerlo. Primero, podemos abrir el panel de Gradle, buscar la carpeta shared/task/build y luego hacemos doble clic en build.

GitHub shared_buid_1

Luego podemos ver la compilación del código en el panel de compilación. Puede ver a Gradle atravesando una serie de etapas y tareas de compilación. Por lo general, esto llevará un poco de tiempo para ejecutarse. Veremos un mensaje de BUILD SUCCESSFUL cuando termine.

GitHub buid_succesful

Para la segunda forma, también podemos ir a la línea de comando a la raíz del proyecto y usar una herramienta llamada Gradle wrapper para ejecutar la misma tarea de compilación.

$ ./gradle: shared: build

Vemos una salida similar y un mensaje BUILD SUCCESSFUL.

GitHub shared_buid_2

Hemos construido con éxito nuestro proyecto compartido, en el que hemos definido una clase de saludo que muestra un saludo personalizado para la plataforma en la que estamos.

Shared library Android

Ahora veremos como implementar la libreria compartida en Android.

En el archivo build.gradle de androidApp, agregamos un packagingOptions dentro del bloque de Android. Esto soluciona un error de compilación que se produciría debido a archivos duplicados en la compilación.

packagingOptions {
    exclude 'META-INF/*-kotlin_module'
}

Luego agregamos una dependencia del proyecto shared.

implementation project(':shared')

Y sincronizamos archivos Gradle.

GitHub buid_gradle_androidApp

En androidApp/src/main/res/layout/activity_main.xml, agregamos un identificador al TextView.

android:id="@+id/greeting"

GitHub activity_main

Luego, en el onCreate del MainActivity, seteamos el texto de la vista, llamando a greeting() del código compartido. Veremos una importación para Greeting y una importación para activity_main usando Kotlin Android Extensions.

GitHub activity_main

Finalmente ejecutamos la app y veremos nuestro saludo que muestra la versión de compilación de Android

GitHub androidAppVersion

Creanción de proyecto para iOS

Ahora implementaremos el código compartido en iOS, pero primero necesitamos configurar el proyecto.

En la terminal en la raiz del proyecto, creamos un directorio para iosApp

GitHub create_iosApp

Luego desde Xcode creamos un nuevo proyecto, seleccionamos iOS App.

  • El nombre del producto es GitHubKMP
  • El identificador de la organización es mobile.world
  • Nos aseguramos de que el lenguaje sea Swift.

GitHub create_project_xcode

Colocamos el proyecto en la nueva carpeta iosApp que acabamos de crear. Una vez creado el proyecto, ahora ejecutamos la aplicación en el simulador de iOS.

A continuación, debemos agregar un task a nuestros archivos de compilación de Gradle que empaquetará el framework para Xcode.

El código del task que necesitamos para Gradle está disponible en la documentación de Kotlin Multiplatform. Copiamos el código y lo pegamos en el archivo build.gradle de shared.

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
    final def framework = kotlin.targets.ios.binaries.getFramework("shared", mode)

    inputs.property "mode", mode
    dependsOn framework.linkTask

    from { framework.outputFile.parentFile }
    into frameworkDir

    doLast {
    new File(frameworkDir, 'gradlew').with {
        text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
        setExecutable(true)
    }
    }
}

tasks.build.dependsOn packForXCode

GitHub task_packForXCode

La primera sección del task establece un directorio para el framework y determina el framework correcto para construir el proyecto Xcode. La siguiente sección copia el archivo del directorio de compilación al directorio del framework. Finalmente, se crea un script bash llamado gradlew en el directorio del framework al que Xcode llama para construir el framework compartido. El script usa la versión del JDK que está incrustada en Android Studio. En la parte inferior del archivo, especificamos que el task de compilación del código compartido depende del nuevo task packForXcode.

GitHub buid_shared_packforxcode

A continuación, damos doble click a build del código compartido. Verificamos que el framework de Xcode empaquetado esté en el directorio esperado.

GitHub xcode_frameworks

Ahora volvemos a Xcode, elegimos el app target y vamos a Configuración general. En la sección de binarios integrados, hacemos clic en el signo más y luego seleccionamos agregar otro.

GitHub add_framework_ios

Navegamos y elegimos el framework compartido. GitHub add_frameworks_ios_file GitHub select_framework_ios

Dado que Kotlin/Native produce binarios nativos completos, debemos deshabilitar la función Bitcode para el proyecto en Build Settings. Seleccionamos el Target GitHubKMP, desde Build Settings, seleccionamos All y buscamos bitcode. Luego cambiamos el valor de Enable Bitcode a No.

GitHub enable_bitcode_ios

Ahora necesitamos actualizar las rutas de búsqueda del framework para el target de iOS. En Build Settings, buscamos Framework Search Paths, luego agregamos el directorio de framework que configuramos en nuestro task de Gradle.

$ (SRCROOT)/../../shared/build/xcode-frameworks

GitHub framework_search_path

El último paso que necesitamos para configurar el proyecto Xcode es agregar un nuevo build phase para que Xcode compile el código compartido. Cambiamos a Build Phases y agregamos un nuevo Run Script.

GitHub new_run_script

En el Run Script, cambiamos al directorio del framework y luego llamamos al script bash que creamos en nuestro task packForXcode, en nuestro task packForXcode, pasando el valor de configuración de Xcode.

cd "$SRCROOT/../../shared/build/xcode-frameworks"
./gradlew :shared:build -PXCODE_CONFIGURATION=${CONFIGURATION}

GitHub run_script_directory

Movemos el nuevo script de ejecución a la parte superior de las fases de compilación.

GitHub move_run_script

Ahora ejecutamos la aplicación para asegurarnos de que no haya errores de compilación debido a ninguno de estos cambios.

Biblioteca compartida de iOS

Ahora, actualizaremos la aplicación de iOS para usar el framework compartido. Como hemos comentado anteriormente, la aplicación de iOS consistirá principalmente en un código de interfaz de usuario, dependiendo del código compartido para hacer la mayor parte del trabajo.

En Xcode en Main.Storyboard, agrego una etiqueta en el centro del guión gráfico y lo cambio de tamaño para darle algunas restricciones predeterminadas.

GitHub add_uilabel

Establecí la propiedad de alineación en el centro de la etiqueta. A continuación, conecto la etiqueta a un IBOutlet llamado greeting en ViewController.swift, que es el controlador de vista de la aplicación.

GitHub connect_uilabel

En el método viewDidLoad() en ViewController, configuramos el texto en la etiqueta de greeting llamando al código compartido, importando el framework shared para poder acceder a la clase Greeting.

GitHub set_text_greeting_ios

Obtenemos la finalización del código en Xcode que proviene del framework compartido de Kotlin. Este código simple es literalmente idéntico a la línea en Kotlin que usamos en la aplicación de Android. Creamos un objeto de saludo y luego llamamos a su método de saludo. Así que ahora puedo crear y ejecutar la aplicación iOS.

GitHub iosAppVersion

Cuando la aplicación se ejecute en el simulador, muestra nuestro muestra el nombre del sistema iOS como se determina en el código compartido.

Conclusión

Ahora tenemos una idea del uso de Kotlin/Native, para qué sirve y como utilizar Kotlin/Native en el contexto de un proyecto multiplataforma de Kotlin. El IDE Android Studio usa Kotlin/Native para compilar código Kotlin compartido para su uso en iOS. Hemos implementado el codigo compartido en ambas plataformas en una función basica, como lo fue mostrar la verison de sistema operativo. En la segunda parte complentaremos el codigo compartido con Networking, Concurrency y Serialization.

El repositorio GitHub para este post lo podemos encontrar aqui

Referencias

Updated:

Comments