Saltar al contenido
K
Entorno y primer proyectoPaso a paso20 min de lectura · 15 min de máquina Básico

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:compileDebugKotlin

Si 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:

androidApp: la dependencia del módulo compartido
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 devices

Paso 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 namespace del módulo androidApp.

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 topResumedActivity
topResumedActivity=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:DEBUG

Lo ú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:

shared/src/
commonMain/kotlin/com/sebastiannarvaez/pokedex/Greeting.ktel «Hola desde»
androidMain/kotlin/com/sebastiannarvaez/pokedex/Platform.android.ktel «Android 35»
androidApp/src/main/kotlin/…/android/
MainActivity.ktquien lo pinta
Dos ficheros y dos source sets para una frase. Ninguno se toca en esta lección.

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:

shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/Greeting.kt
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:

shared/src/androidMain/kotlin/com/sebastiannarvaez/pokedex/Platform.android.kt
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:compileAndroidMain aparece antes que :androidApp:compileDebugKotlin (con o sin UP-TO-DATE al final).
  • adb devices lista tu emulador (y tu teléfono, si lo conectaste) con el estado device.
  • ./gradlew :androidApp:installDebug termina en BUILD SUCCESSFUL con el emulador ya arrancado.
  • adb shell am start -n com.sebastiannarvaez.pokedex/.android.MainActivity abre la app; con .MainActivity a secas responde Activity 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.sdk contesta el mismo número).
  • En Logcat, la consulta package:mine level:DEBUG se 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. 1.Pulsas Run con la configuración androidApp. En la pestaña Build, ¿qué tarea de compilación aparece primero?

  2. 2.adb shell am start -n com.sebastiannarvaez.pokedex/.MainActivity responde Activity class … does not exist. ¿Por qué?

  3. 3.Compilas con compileSdk = 37 y en el emulador de API 35 la pantalla dice «Hola desde Android 35». ¿Qué lee Build.VERSION.SDK_INT?

  4. 4.¿En qué source set vive la clase Greeting que forma el «Hola desde …» y por qué se ve igual en las dos plataformas?

  5. 5.¿Qué filtra exactamente la consulta package:mine en Logcat?

Faltan 5 de 5