La primera dependencia
Al terminar: Añadir Kermit al código compartido y ver el mismo log en Logcat y en la consola de Xcode.
Hasta ahora el módulo compartido no dependía de nada: solo Kotlin y lo que trae el propio compilador. Al terminar esta lección tendrá su primera librería externa, y la misma línea de código, escrita una sola vez, aparecerá en Logcat y en la consola de Xcode.
La librería es Kermit, el registro multiplataforma de Touchlab. Pero lo que se aprende aquí no es Kermit: es la receta para añadir cualquier dependencia a commonMain. Comprobar que publica tus targets, declararla en el catálogo, ponerla en el source set, saber qué decide api frente a implementation y comprobar que el resultado llega vivo a las dos plataformas. Ese último paso tiene una trampa, que verás en el paso 7: un registro que existe pero que una consulta no enseña.
Paso 1 · Qué instalas y por qué ahora
Kermit es una librería de registro de mensajes (logging): una llamada como registro.i { "hola" } y el mensaje sale por el sitio que corresponde a cada plataforma. Se instala en el módulo compartido porque el código común quiere registrar sin saber dónde corre, y se instala ahora porque, a partir de aquí, hay código que corre en dos plataformas y conviene poder ver qué hace en cada una. En el paso 6 lo usará Greeting.
Paso 2 · Comprueba que publica tus targets antes de escribir nada
Una librería de Kotlin Multiplatform no publica un solo archivo (un artefacto, en la jerga): publica uno por target, y un fichero .module que dice cuáles hay. Si la librería no publica ios_simulator_arm64, tu proyecto compilará en Android y fallará en cuanto Gradle compile para iOS, con un No matching variant of … was found (el error 4 de la caja «Si Sync sale en rojo», más abajo).
curl -s https://repo1.maven.org/maven2/co/touchlab/kermit/maven-metadata.xml | grep -E "<latest>|<release>"curl -s descarga la página (el -s es «sin barra de progreso»). El | pasa lo descargado al comando siguiente, y grep -E deja solo las líneas que contienen alguna de las dos palabras (el | de dentro de las comillas significa «o»). Verás dos líneas, <latest> y <release>, con el número de versión entre las etiquetas; es el número que se usa en el paso 3.
Los dos que le importan a este proyecto son ios_arm64 e ios_simulator_arm64. Para verlos con tus ojos, abre en el navegador https://repo1.maven.org/maven2/co/touchlab/kermit/2.2.0/kermit-2.2.0.module y busca esas dos palabras. Esa comprobación tarda medio minuto y te ahorra un error larguísimo en el que el target solo aparece escondido entre atributos.
Paso 3 · Las dos líneas del catálogo
Abre gradle/libs.versions.toml. Kermit tiene su propio número de versión, así que hacen falta dos líneas, cada una en su sección: la versión, al final de [versions], y la librería, al final de [libraries] (antes de la línea en blanco y de la cabecera [plugins]).
kermit = "2.2.0"kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }module junta las dos mitades de las coordenadas (grupo:nombre) y version.ref = "kermit" dice «la versión es la que se llama kermit en [versions]». El alias kermit se usará como libs.kermit. No es un plugin, así que no se toca el build.gradle.kts de la raíz.
Paso 4 · La dependencia, en commonMain
Abre shared/build.gradle.kts (en la vista Project, dentro de la carpeta shared). Dentro de kotlin { } ya hay un bloque sourceSets { } con commonTest.dependencies. Deja ese bloque como el de abajo: la parte nueva es commonMain.dependencies, que va antes de commonTest.dependencies.
sourceSets {
commonMain.dependencies { // librerías del código común: la usan TODOS los targets
api(libs.kermit) // api: los demás módulos (androidApp) y el framework de iOS pueden usarla; ver abajo
}
commonTest.dependencies { // ya estaba: librerías de los tests comunes
implementation(kotlin("test"))
}
}Una sola línea, y Gradle resuelve una variante distinta por cada target (lo viste con kotlin("test") en Gradle sin dolor). Pulsa Sync Now.
api, implementation y export
Aquí hay dos destinatarios de lo que shared ofrece: androidApp, que es otro módulo de Gradle, y Swift, que lee el framework de iOS (el paquete que Kotlin/Native construye con tu código para que Xcode lo use; enlazarlo es unirlo a la app). Para decidir entre api e implementation hace falta una idea nueva: la firma pública, que es lo que se ve de una función o clase desde fuera del módulo: su nombre, sus parámetros y lo que devuelve. Cuando un tipo de una librería aparece ahí, decimos que asoma. Un ejemplo mínimo, con datos ajenos al proyecto:
// Ambas funciones están en shared, en commonMain, y son públicas.
fun saludo(): String = "hola" // firma: saludo(), devuelve String.
// Solo tipos de Kotlin: no asoma nada.
fun registroSonda(): Logger = Logger.withTag("sonda") // firma: registroSonda(), devuelve un Logger.
// Logger es un tipo de Kermit: asoma.Con esa segunda función, lo que se midió en el proyecto fue esto:
androidAppconapi(libs.kermit): compila y puede escribirregistroSonda().i { "hola" }. Conimplementation(libs.kermit)falla conCannot access class 'co.touchlab.kermit.Logger'. Check your module classpath for missing or conflicting dependencies.: la app ve la firma, pero no tiene la librería para entender el tipo.- El framework de iOS ya contiene
Logger, conapiy conimplementation, pero con un nombre con prefijo. Es la línea que Kotlin escribe enShared.h, el fichero que describe a Swift lo que el framework ofrece:
+ (SharedKermitLogger *)registroSonda __attribute__((swift_name("registroSonda()")));- Para el nombre limpio hay que exportar.
export(libs.kermit)va dentro del bloque del framework enshared/build.gradle.kts, y le dice «los tipos de esta librería llegan a Swift con su nombre, sin prefijo»:
iosTarget.binaries.framework {
baseName = "Shared"
export(libs.kermit) // solo se puede exportar una dependencia declarada con api
}+ (SharedLogger *)registroSonda __attribute__((swift_name("registroSonda()")));Y con export y implementation a la vez, el enlazado se detiene con Following dependencies exported in the debugFramework binary are not specified as API-dependencies of a corresponding source set. Por eso export exige api: api es la condición, no la exportación.
La regla es: implementation por defecto, y api cuando un tipo de la librería asoma en una firma pública. Mirar si compila hoy no basta: si el consumidor recibe la librería por otro camino, con implementation también compila, y el día que deje de recibirla falla. Lo que cuenta es lo que shared promete en sus firmas.
En este proyecto Log (el paso 5) esconde Kermit: sus funciones solo reciben y devuelven String y Throwable?, y el Logger queda dentro, privado. Ningún tipo de Kermit asoma, y por eso hoy bastaría implementation. El proyecto declara api(libs.kermit) por prudencia, para no cambiarla el día que androidApp o Swift necesiten un tipo de la librería; api no es «lo seguro», porque cada dependencia api la heredan los consumidores. Si un ejemplo posterior del curso usa una palabra distinta de la que pediría la regla, se explica allí con su motivo.
Sync Now y comprobar
Sync Now debe terminar sin errores rojos. Para ver que la librería ha llegado, y a cuál de sus variantes, en la Terminal. :shared:dependencies pide a Gradle el árbol de dependencias del módulo shared, y --configuration iosSimulatorArm64CompileKlibraries lo limita a lo que se usa al compilar para el simulador de iOS (un klib es el formato de librería de Kotlin/Native):
./gradlew :shared:dependencies --configuration iosSimulatorArm64CompileKlibrariesiosSimulatorArm64CompileKlibraries - Compile classpath for 'iosSimulatorArm64/main'.
+--- co.touchlab:kermit:2.2.0
| \--- co.touchlab:kermit-iossimulatorarm64:2.2.0
| +--- co.touchlab:kermit-core:2.2.0
| | \--- co.touchlab:kermit-core-iossimulatorarm64:2.2.0
| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.3.21 -> 2.4.20
| \--- org.jetbrains.kotlin:kotlin-stdlib:2.3.21 -> 2.4.20
\--- org.jetbrains.kotlin:kotlin-stdlib:2.4.20El árbol se lee de arriba abajo: cada +--- o \--- es una dependencia, y lo que cuelga con más sangría es lo que esa trae. Una línea tuya, y para iOS Gradle ha escogido kermit-iossimulatorarm64. Repite la consulta con jvmCompileClasspath y saldrá kermit-jvm; con iosArm64CompileKlibraries, la del iPhone. La flecha 2.3.21 -> 2.4.20 es Gradle alineando la librería estándar de Kotlin con la de tu proyecto: Kermit se compiló con Kotlin 2.3.21 y tu proyecto usa 2.4.20.
Paso 5 · Un Logger propio, no el estático
Kermit trae un Logger global listo para usar. No lo usamos: el proyecto envuelve uno propio en un object (el singleton que viste en object, companion y vararg). El fichero usa además argumentos con nombre y por defecto (Funciones y parámetros), Throwable? (Nulabilidad, Excepciones: try, catch y require) y una lambda final fuera de los paréntesis (Lambdas y funciones de orden superior).
Antes de escribir el fichero, un ejemplo mínimo de Kermit con datos ajenos al proyecto. Un nivel es la importancia de un mensaje (de menos a más: depuración, información, aviso, error) y el umbral (minSeverity) es el nivel por debajo del cual el mensaje no sale.
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.kermit.loggerConfigInit
import co.touchlab.kermit.platformLogWriter
val registro = Logger(
config = loggerConfigInit(platformLogWriter(), minSeverity = Severity.Info), // umbral: de Info para arriba
tag = "Tienda", // la etiqueta que filtras en Logcat
)
registro.d { "carrito abierto" } // Debug: por debajo del umbral, no sale
registro.i { "3 productos en el carrito" } // Info
registro.w { "queda 1 litro de leche" } // Warn: aviso
registro.e(IllegalStateException("sin stock")) { "no se pudo pagar" } // Error, con la excepción que lo causóSalida real (se ejecutó dentro de un test de la JVM, donde Kermit escribe en la consola; no hace falta que lo repitas):
Info: (Tienda) 3 productos en el carrito
Warn: (Tienda) queda 1 litro de leche
Error: (Tienda) no se pudo pagar
java.lang.IllegalStateException: sin stockEn Logcat las mismas líneas salen como I Tienda : …, W Tienda : … y E Tienda : …. El Log de abajo no es el Log de Android: es un object tuyo, en el paquete core, y por eso no lleva etiqueta en cada llamada (la pone una vez).
Una diferencia con el ejemplo: aquí d e i pasan el texto suelto, y w y e lo pasan dentro de una lambda { mensaje }. Es porque w y e reciben además la causa (la excepción), y Kermit pide la causa primero y el mensaje al final, en lambda; d e i no tienen causa y usan la forma corta.
Crea el fichero como en El módulo compartido y la prueba de humo: clic derecho sobre shared → New → File y escribe la ruta completa src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/Log.kt (Android Studio crea la carpeta core si no existe). Plan B desde la Terminal de Android Studio: mkdir -p shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core y touch shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/core/Log.kt. Ábrelo y pega esto, entero:
package com.sebastiannarvaez.pokedex.core
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.kermit.loggerConfigInit
import co.touchlab.kermit.platformLogWriter
/**
* El registro de la app, con una sola etiqueta para todo.
*
* Se usa un `Logger` propio y no el estatico de Kermit porque el estatico es
* estado global: dos modulos que lo configuren distinto se pisan, y en un test
* no hay forma de silenciarlo.
*
* `platformLogWriter()` resuelve solo el destino de cada plataforma: Logcat en
* Android y `os_log` en iOS. No hay que escribir nada por plataforma.
*/
object Log {
private val logger: Logger = Logger(
config = loggerConfigInit(
platformLogWriter(),
// En una compilacion de release este umbral sube, para no dejar en
// el registro del dispositivo nada que no deba estar ahi.
minSeverity = Severity.Debug,
),
tag = "Pokedex",
)
fun d(mensaje: String) = logger.d(mensaje)
fun i(mensaje: String) = logger.i(mensaje)
fun w(mensaje: String, causa: Throwable? = null) = logger.w(causa) { mensaje }
fun e(mensaje: String, causa: Throwable? = null) = logger.e(causa) { mensaje }
}Los comentarios del fichero ya dicen el porqué de cada decisión. Una aclaración sobre el del umbral: Severity.Debug deja pasar todos los niveles, y es lo que usa el proyecto mientras se desarrolla; la subida del umbral para una app publicada es una decisión que se toma al publicar, no algo que el código haga solo. Dos detalles más, línea a línea:
// platformLogWriter() elige solo el destino: Logcat en Android y `os_log` (el registro del sistema de Apple) en iOS.
// No hace falta escribir código distinto por plataforma (lo que la lección siguiente llama expect/actual):
// antes de dividir el código por plataforma, mira si tu librería ya lo resolvió.
platformLogWriter(),
// El mensaje va en una lambda { mensaje }: Kermit solo la ejecuta si el nivel está activo,
// así que no se construye un texto que nadie va a leer.
fun w(mensaje: String, causa: Throwable? = null) = logger.w(causa) { mensaje }Paso 6 · Llamarlo desde el saludo
Abre Greeting.kt (en commonMain, paquete com.sebastiannarvaez.pokedex) y sustituye su contenido: pasa a devolver la frase y a registrarla. Es el fichero entero:
package com.sebastiannarvaez.pokedex
import com.sebastiannarvaez.pokedex.core.Log
/**
* La prueba de humo del proyecto: si esta frase aparece en las dos pantallas,
* el codigo compartido esta llegando a Android y a iOS.
*/
class Greeting {
private val platform = currentPlatform()
fun greet(): String {
val saludo = "Hola desde ${platform.name}"
// La misma linea de codigo acaba en Logcat y en la consola de Xcode.
Log.i(saludo)
return saludo
}
}Paso 7 · Comprobarlo en las dos plataformas
En Android sale a la primera. Ejecuta la app y pide el registro filtrando por la etiqueta:
# adb es la herramienta de Android que habla con el emulador o el móvil conectado.
# logcat es su registro; -d lo vuelca y termina; -s deja solo las líneas con esa etiqueta
adb logcat -d -s "Pokedex"09-10 18:37:32.613 14875 14875 I Pokedex : Hola desde Android 35En iOS, ejecuta la app desde Xcode (Cmd+R). La forma más fácil de ver el registro es la consola de Xcode: si no ves la zona de texto bajo el editor, ábrela con View → Debug Area → Activate Console. Ahí aparece la línea de Kermit sin hacer nada más. Desde la Terminal, la consulta equivalente no la muestra. <UDID> es el identificador del simulador arrancado (se explica en la última caja de este paso), y el nombre de la app, Pokedex, es el del esquema que ves junto al botón Run.
xcrun simctl es la herramienta de Apple que controla los simuladores desde la Terminal; spawn ejecuta un comando dentro del simulador, aquí log show, que lee su registro.
xcrun simctl spawn <UDID> log show --last 2m --predicate 'process == "Pokedex"'Punto de control
libs.versions.tomldeclarakermit = "2.2.0"en[versions]y la entradakermiten[libraries], cada una en su sección.- En
shared/build.gradle.kts, la dependencia entra encommonMain.dependenciesconapi(libs.kermit). ./gradlew :shared:dependencies --configuration iosSimulatorArm64CompileKlibrariesmuestraco.touchlab:kermit-iossimulatorarm64:2.2.0../gradlew :androidApp:assembleDebugtermina enBUILD SUCCESSFUL.adb logcat -d -s "Pokedex"enseña la líneaI Pokedex : Hola desde Android N.- En la consola de Xcode aparece la línea con el emoji y la etiqueta
Pokedex; en la Terminal,log showcon--info --debugenseña la misma frase, y sin esas banderas, no. Log.ktno usa elLoggerestático de Kermit: construye el suyo conloggerConfigInit(...).
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Qué se comprueba en el fichero
.modulede una librería antes de añadirla acommonMain?2.Kermit se declara con
api(libs.kermit)encommonMain. ¿Qué consigueapique no consigueimplementation?3.El proyecto crea su propio
Loggeren vez de usar el estático de Kermit. ¿Cuál es la razón práctica?4.
platformLogWriter()escribe en Logcat en Android y enos_logen iOS, y tú no escribes ni una línea distinta por plataforma. ¿Qué enseña eso?5.En el simulador,
xcrun simctl spawn <UDID> log show --last 2m --predicate 'process == "Pokedex"'no enseña ninguna línea de Kermit. ¿Qué está pasando?