Ejecutar en Android
Al terminar: Ejecutar la app de un proyecto multiplataforma en Android y seguir el camino del saludo desde commonMain hasta la pantalla.
Ya has ejecutado apps en el emulador de Android: creaste el dispositivo virtual en Instalar Android Studio y el SDK y arrancaste Marcador por primera vez en La Activity. Esa mecánica no cambia, así que aquí no se repite.
Lo que sí cambia es lo que hay detrás del botón Run en un proyecto Kotlin Multiplatform: la app de Android ya no es un único módulo, sino un módulo (androidApp) que depende de otro (shared), y lo primero que compila es el segundo. Al terminar tendrás en el emulador el saludo «Hola desde Android N», sabrás de qué fichero sale cada mitad de esa frase y cómo lanzar la app desde la Terminal cuando el IDE no colabore.
Paso 1 · Run, con la configuración androidApp
En la barra superior de Android Studio, a la izquierda del botón verde, hay una lista de configuraciones de ejecución (una configuración le dice al IDE qué lanzar). En Marcador se llamaba app; aquí el módulo de Android se llama androidApp, y es la que debes elegir. Elige también tu dispositivo virtual en la lista de al lado, arranca el emulador si hace falta y pulsa Run (el triángulo verde; Ctrl + R en macOS).
Tarda un poco más que en Marcador, porque Gradle compila primero el módulo compartido y después la app. Si abres la pestaña Build (abajo), verás, entre otras muchas líneas, estas tareas en ese orden:
> Task :shared:compileAndroidMain
> Task :shared:bundleAndroidMainClassesToCompileJar
> Task :androidApp:compileDebugKotlinSi ya compilaste en la lección anterior, algunas pueden salir con UP-TO-DATE al final (Gradle avisa de que no hay nada nuevo que compilar). Es normal y el orden es el mismo. Si quieres verlas trabajar, usa Build → Clean Project (borra lo compilado) y pulsa Run otra vez.
compileAndroidMain compila el código de shared que se ejecuta en Android: lo de commonMain más lo de androidMain. Es la prueba de que la app depende del módulo compartido. Esa dependencia está en una línea de androidApp/build.gradle.kts:
dependencies {
// `projects.shared` es el módulo `shared` escrito como propiedad: funciona
// porque `settings.gradle.kts` activa TYPESAFE_PROJECT_ACCESSORS (si no,
// se escribe `project(":shared")`, que significa lo mismo).
// `implementation` = androidApp usa shared por dentro; lo que dependa de
// androidApp no lo ve (no se lo pasa).
implementation(projects.shared)
}Todo lo que hace el botón Run tiene su equivalente en la Terminal (la de Android Studio, abajo, o cualquier otra), y conviene conocerlo para cuando el IDE no colabore.
Desde la carpeta del proyecto (cd ~/Pokedex-KMP):
# Compila `shared` y `androidApp` e instala la app en el emulador o móvil conectado
./gradlew :androidApp:installDebug
# Lanza la pantalla principal a mano (el paso 2 explica el nombre que lleva)
adb shell am start -n com.sebastiannarvaez.pokedex/.android.MainActivity
# ¿Qué hay conectado ahora mismo?
adb devicesPaso 2 · Dos nombres que no coinciden
Mira bien el comando am start de arriba: com.sebastiannarvaez.pokedex/.android.MainActivity. El componente lleva dos partes separadas por /, y en este proyecto no son lo mismo:
- A la izquierda va el
applicationId: la identidad de la app en el móvil. - A la derecha va la clase de la Activity, que vive en el paquete que declara el
namespacedel móduloandroidApp.
Los dos valores están en androidApp/build.gradle.kts, dentro del bloque android { … }. Ábrelo y míralos: en tu proyecto son applicationId = "com.sebastiannarvaez.pokedex" y namespace = "com.sebastiannarvaez.pokedex.android", los que lleva el comando de arriba.
El punto inicial es una abreviatura del paquete de la izquierda. Por eso .MainActivity no vale: significaría com.sebastiannarvaez.pokedex.MainActivity, que no existe (la clase está un paquete más abajo). Si lo lanzas así, Android responde:
Starting: Intent { cmp=com.sebastiannarvaez.pokedex/.MainActivity }
Error type 3
Error: Activity class {com.sebastiannarvaez.pokedex/com.sebastiannarvaez.pokedex.MainActivity} does not exist.La forma correcta añade lo que falta del namespace: .android.MainActivity. Es también como la imprime el sistema cuando le preguntas qué hay en primer plano:
# `dumpsys` vuelca el estado interno de un servicio de Android; `activity` es el
# que gobierna las pantallas. El `|` (tubería) pasa lo que imprime el primer
# comando al segundo; `grep` se queda solo con la línea que nos interesa.
adb shell dumpsys activity activities | grep topResumedActivitytopResumedActivity=ActivityRecord{… u0 com.sebastiannarvaez.pokedex/.android.MainActivity …}Paso 3 · Logcat en un proyecto con dos módulos
Logcat funciona como en El ciclo de vida de la Activity: View → Tool Windows → Logcat, y en el campo de consulta escribe:
package:mine level:DEBUGLo único que añade KMP: el código de shared no tiene un proceso aparte. Se ejecuta dentro de la app, así que sus mensajes saldrán en Logcat con el mismo paquete que los de androidApp, y package:mine los recogerá todos. Ahora mismo ni shared ni androidApp escriben ningún mensaje, así que es normal que la lista salga casi vacía: aquí solo se aprende la consulta. En La primera dependencia verás la misma línea de log en Logcat y en la consola de Xcode.
Paso 4 · Un teléfono real (opcional)
El emulador vale para casi todo, pero hay cosas (cámara, sensores, rendimiento real) que solo se ven en hardware. Con la depuración por USB activada, conecta el cable y acepta, en el teléfono, el diálogo de la clave RSA (la huella que autoriza a tu ordenador).
Comprueba con adb devices: debe salir el teléfono con estado device. Si sale unauthorized, el diálogo sigue esperando o lo rechazaste: desconecta, vuelve a conectar y desbloquea la pantalla. Después elige el teléfono en la lista de dispositivos y pulsa Run, igual que con el emulador. Los nombres de los menús del teléfono cambian un poco según el fabricante.
El resultado
En la pantalla se lee esta frase:
Hola desde Android 35(El número es el de tu emulador: el de la imagen del sistema que elegiste al crearlo; 35 es el del ejemplo de esta guía, y la imagen más reciente con Google Play puede ser otra.) Puedes saberlo sin abrir la app: adb shell getprop ro.build.version.sdk contesta el mismo número (aquí, 35). Ninguno de los ficheros que producen la frase cambia en esta lección. Vale la pena mirar de dónde sale cada mitad:
Las rutas de los títulos de abajo son las del proyecto en disco. commonMain y androidMain son source sets (carpetas de código que se compilan para unas plataformas concretas), como contó Qué es KMP y qué no; Anatomía del proyecto los recorre uno a uno.
La frase entera vive en commonMain y no sabe nada de Android:
package com.sebastiannarvaez.pokedex
/**
* 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 = "Hola desde ${platform.name}"
}El número lo pone androidMain, que sí sabe dónde está corriendo:
package com.sebastiannarvaez.pokedex
import android.os.Build
private class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
}
actual fun currentPlatform(): Platform = AndroidPlatform()El proyecto compila contra la API 37 (compileSdk) y en pantalla lees 35 porque SDK_INT no pregunta contra qué compilaste: pregunta dónde estás corriendo. La misma app instalada en otro emulador dirá otro número sin recompilar nada. (Lo que sí tiene que cuadrar es el mínimo: la imagen del emulador debe ser de API 24 o superior, el minSdk del proyecto.)
Punto de control
- En la lista de configuraciones de la barra superior aparece androidApp, y con ella la app arranca en el emulador.
- En la pestaña Build,
:shared:compileAndroidMainaparece antes que:androidApp:compileDebugKotlin(con o sinUP-TO-DATEal final). adb deviceslista tu emulador (y tu teléfono, si lo conectaste) con el estadodevice../gradlew :androidApp:installDebugtermina enBUILD SUCCESSFULcon el emulador ya arrancado.adb shell am start -n com.sebastiannarvaez.pokedex/.android.MainActivityabre la app; con.MainActivitya secas respondeActivity class … does not exist.- En la pantalla se lee la frase «Hola desde Android N», donde N es la API de tu imagen, no la 37 (
adb shell getprop ro.build.version.sdkcontesta el mismo número). - En Logcat, la consulta
package:mine level:DEBUGse acepta sin error en el campo de consulta; que salgan pocas líneas (o ninguna) es lo esperado, porque tu app aún no escribe mensajes.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Pulsas Run con la configuración
androidApp. En la pestaña Build, ¿qué tarea de compilación aparece primero?2.
adb shell am start -n com.sebastiannarvaez.pokedex/.MainActivityrespondeActivity class … does not exist. ¿Por qué?3.Compilas con
compileSdk = 37y en el emulador de API 35 la pantalla dice «Hola desde Android 35». ¿Qué leeBuild.VERSION.SDK_INT?4.¿En qué source set vive la clase
Greetingque forma el «Hola desde …» y por qué se ve igual en las dos plataformas?5.¿Qué filtra exactamente la consulta
package:mineen Logcat?