Saltar al contenido
K
Android: la plataformaPaso a paso35 min de lectura · 8 min de máquina Intermedio

La Activity

Al terminar: Hacer que :app dependa de :torneo y ejecutar Marcador por primera vez en el emulador, con la tabla de :torneo en pantalla.

Al terminar esta lección, Marcador será una app Android de verdad y la habrás ejecutado por primera vez en el emulador: :app enseñará en pantalla la tabla de posiciones que calcula :torneo. En la lección siguiente, El ciclo de vida de la Activity, mirarás por dentro cuándo crea, pausa y destruye el sistema esa pantalla.

Importa porque en Android no eres tú quien decide cuándo empieza y acaba una pantalla: lo decide el sistema, y tu código solo reacciona. Además, es la primera lección que toca :app y la primera que conecta los dos módulos: :app pasará a depender de :torneo. Temas: la Activity; dependencia entre módulos; Service, BroadcastReceiver y ContentProvider (recuadro); el Manifest por encima; ejecutar la app en el emulador.

Una Activity es una pantalla que crea el sistema

Una app Android no es un programa con un main() que tú arrancas. Es un conjunto de componentes: piezas que el sistema sabe crear por separado. La primera, y la que vas a usar todo el módulo, es la Activity (actividad).

Tres palabras que se repiten desde aquí (la tercera es el tema de la lección siguiente):

  • Proceso: el programa en ejecución que Android crea para tu app. Dentro viven tus Activity.
  • Callback: un método tuyo que llama el sistema, no tú. Tú lo sobrescribes; él decide cuándo.
  • Ciclo de vida (lifecycle): la secuencia de callbacks por la que pasa una Activity desde que nace hasta que muere.

El Manifest, por encima

AndroidManifest.xml es la ficha de la app: lo primero que lee Android para saber qué pantallas y servicios tiene. Como la ficha de un hotel en recepción: si una habitación no está en ella, para el recepcionista no existe. Si una Activity no aparece ahí, el sistema no sabe que existe y la app se cierra al intentar abrirla. Ábrelo con doble clic en app → manifests → AndroidManifest.xml. El manifiesto está escrito en XML, un formato de texto hecho de etiquetas: <activity …> abre una, </activity> la cierra y lo de dentro le pertenece (una etiqueta que acaba en /> se abre y se cierra a la vez, y no contiene nada). Dentro de la etiqueta de apertura van sus atributos, en la forma nombre="valor", por ejemplo android:name=".MainActivity". Y <!-- … --> es un comentario: Android lo ignora. Lo que importa hoy es esta parte de la plantilla, sin tocarla:

AndroidManifest.xml: la plantilla (extracto)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Raíz del fichero. Dentro va todo lo que tiene la app. -->
 
    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.Marcador">
        <!-- <application> es la app entera: aquí van sus datos generales (nombre,
             icono, tema) y la lista de componentes. Hay una por manifiesto. -->
 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Marcador"
            android:windowSoftInputMode="adjustResize">
            <!-- <activity> declara UNA pantalla. Por eso MainActivity aparece aquí:
                 es la clase `MainActivity` de la plantilla, y esta línea se la presenta a Android.
                 El punto de ".MainActivity" significa «dentro del paquete de la app». -->
 
            <intent-filter>
                <!-- Un intent-filter dice «a qué avisos responde esta pantalla».
                     Estos dos juntos significan «soy la pantalla de entrada,
                     la que se abre al tocar el icono de la app». -->
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
TrozoQué dice
<application>La app entera; contiene a todos sus componentes
<activity android:name=".MainActivity">Existe una Activity con esa clase
<intent-filter> con MAIN y LAUNCHER«Esta es la pantalla que se abre al tocar el icono»
android:exported="true"Otras apps pueden arrancarla; aquí lo hace el lanzador del sistema (la pantalla con los iconos), que es otra app

El resto de atributos (label, theme, windowSoftInputMode) no los necesitas hoy. Un valor que empieza por @, como @mipmap/ic_launcher o @string/app_name, es una referencia a un recurso del proyecto (un icono, un texto) que vive en otra carpeta; por ahora basta saber que existe. No cambies nada de ese fichero. Los permisos y los filtros de intención se explican en Intents y en Permisos.

Un ejemplo sencillo: la pantalla de una tienda

Antes de tocar tu proyecto, mira una Activity completa que no tiene nada que ver con Marcador. Es la pantalla de una tienda: muestra el título y una lista de tres productos. Usa las mismas piezas que escribirás en un momento, y cada línea lleva su explicación.

Antes de leerla, cinco palabras que aparecen en ella y que no has visto todavía (no hace falta memorizarlas: la lección de Compose las estudia con calma):

  • Jetpack: el conjunto de librerías oficiales de Android. Sus import empiezan por androidx.
  • Compose: la librería de Jetpack con la que se dibuja la pantalla escribiendo código Kotlin, sin ficheros de diseño aparte. setContent { … } dice qué se dibuja: lo que va entre sus llaves es la pantalla. Text pinta un texto y Column apila elementos en vertical.
  • Modifier: un ajuste que se le pone a un elemento (margen, tamaño…). Modifier.padding(16.dp) lo separa del borde.
  • dp: la unidad de tamaño de Android. Un dp ocupa más o menos lo mismo en un móvil pequeño que en uno grande, porque se ajusta a cuántos píxeles caben por pulgada en esa pantalla (la densidad). Por eso se usa en vez de píxeles.
  • MaterialTheme: aplica los colores y las tipografías de Material, el estilo visual de Google, a todo lo que lleva dentro.
ejemplo mínimo: la pantalla de una tienda (con comentarios)
import android.os.Bundle                                   // el paquete de datos guardados que recibe onCreate
import androidx.activity.ComponentActivity                 // la Activity de Jetpack, de la que se hereda
import androidx.activity.compose.setContent                // la función que dice qué se dibuja
import androidx.compose.foundation.layout.Column           // apila elementos en vertical
import androidx.compose.foundation.layout.padding          // margen alrededor
import androidx.compose.material3.MaterialTheme            // colores y tipografías
import androidx.compose.material3.Text                     // un texto en pantalla
import androidx.compose.ui.Modifier                        // ajustes de un elemento (margen, tamaño…)
import androidx.compose.ui.unit.dp                         // la unidad de tamaño «dp»
 
class TiendaActivity : ComponentActivity() {               // tu pantalla: hereda de la Activity de Jetpack
 
    override fun onCreate(savedInstanceState: Bundle?) {   // el callback: Android lo llama al crear la pantalla
        super.onCreate(savedInstanceState)                 // primero, el trabajo de base de la clase madre
 
        setContent {                                       // lo que hay entre las llaves es lo que se dibuja
            MaterialTheme {                                // aplica colores y tipografías a lo de dentro
                Column(Modifier.padding(16.dp)) {          // apila los elementos, con 16 dp de margen
                    Text("Tienda")                         // el título
                    listOf("Pan", "Leche", "Huevos").forEach { producto ->  // por cada producto de la lista…
                        Text("· $producto")                // …un texto con un punto delante
                    }
                }
            }
        }
    }
}

Para que Android la conozca, su entrada en el manifiesto es esta (dentro de <application>, junto a la de MainActivity). Sin esta línea, abrirla cierra la app:

ejemplo mínimo: la entrada de la tienda en el manifiesto
<activity android:name=".TiendaActivity" />

Si Android abriera esa pantalla, en el móvil se vería esto:

Tienda
· Pan
· Leche
· Huevos

Para pensar: nadie ha escrito TiendaActivity() en ningún sitio. Entonces, ¿quién crea la pantalla?

La crea el sistema. Tú solo describes la clase y la anuncias en el manifiesto; cuando hace falta abrirla, Android la construye por su cuenta y llama a onCreate. Fíjate también en el orden dentro de onCreate: super.onCreate(…) va primero, para que la clase madre haga su trabajo antes de que añadas lo tuyo. Aquí no hay que escribir nada: en el paso siguiente usarás exactamente estas piezas en tu proyecto.

Paso · Haz que :app dependa de :torneo

Objetivo: que el código de :app pueda usar las clases de :torneo, con una sola línea en el build.gradle.kts de :app.

Qué es una dependencia entre módulos. Hasta ahora :app y :torneo son dos desconocidos: Gradle compila cada uno por separado y :app no puede ver ni una clase de :torneo. Si escribieras en :app un import de Ejemplo o de Torneo, saldría en rojo. Para que las vea, :app tiene que declararlo: «yo uso :torneo».

Es lo mismo que haces al instalar una librería, con una diferencia:

  • Una librería de internet se identifica por sus coordenadas (grupo, nombre y versión) y Gradle la descarga.
  • Un módulo del proyecto se identifica por su ruta, :torneo (el mismo nombre de la línea include(":torneo") de settings.gradle.kts), y no se descarga nada porque ya está en tu proyecto.

Por eso no hay versión ni entrada en el catálogo: se usa siempre el código que tienes ahora mismo.

La línea que lo declara es esta:

build.gradle.kts (Module :app): la línea nueva
implementation(project(":torneo"))   // implementation: entra en el código y en la app; project(":torneo"): el módulo con esa ruta

project(":torneo") significa «el módulo del proyecto que se llama :torneo», y implementation es la palabra que viste en Los ficheros de Gradle de tu proyecto, frente a testImplementation: el módulo entra en el código de :app y en la app final, no solo en sus tests.

La dirección de la flecha. La línea se escribe en el fichero del módulo que usa, no en el del módulo usado:

:app  ──implementation(project(":torneo"))──▶  :torneo
(escribe la línea)                             (no escribe nada)
 
:app ve las clases públicas de :torneo.
:torneo no sabe que :app existe.

Esto es lo que dice Tu primer proyecto en Kotlin en «Módulo frente a carpeta»: :torneo es Kotlin puro y no sabe nada de Android, así que no puede usar nada de :app; en cambio :app pinta pantallas y necesita la lógica. Por eso la línea va en build.gradle.kts (Module :app) y no en el de :torneo.

Gradle Scripts
build.gradle.kts (Module :app)se modifica: dependencies
El fichero de esta instalación, tal como lo ves en la vista Android. No hay que tocar el catálogo.
  1. Abre Gradle Scripts → build.gradle.kts (Module :app) con doble clic.
  2. Baja hasta el bloque dependencies { … }. Añade la línea nueva como primera línea del bloque, justo después de la llave de apertura. El orden entre dependencias no importa; la primera línea es solo un sitio fácil de encontrar.
build.gradle.kts (Module :app): el bloque dependencies, después (solo el principio)
dependencies {
    implementation(project(":torneo"))                     // la línea nueva: :app usa el módulo :torneo
    implementation(platform(libs.androidx.compose.bom))     // de la plantilla: el BOM fija las versiones de Compose
    implementation(libs.androidx.activity.compose)          // de la plantilla: ComponentActivity y setContent
    // … el resto no cambia …
}

Las demás líneas de dependencies son de la plantilla de Android Studio. La de platform(libs.androidx.compose.bom) es el BOM de Compose, una lista de versiones de las librerías de Compose que van bien juntas, y por eso esas librerías no llevan número.

  1. Pulsa Sync Now, en la barra que aparece arriba del editor. Debe terminar sin errores rojos, y rápido: no descarga nada. Android Studio también puede escribir esta línea por ti desde File → Project Structure…, en el apartado de dependencias del módulo app; aprenderla a mano te permite leer lo que escriba el IDE.
  2. Comprueba. En la Terminal de Android Studio (View → Tool Windows → Terminal) pide a Gradle la lista de lo que usa :app al ejecutarse en su versión debug (la que ejecutas mientras desarrollas; la release es la que se publica). --configuration debugRuntimeClasspath es solo el nombre de esa lista:
./gradlew :app:dependencies --configuration debugRuntimeClasspath | grep "project "

La barra | pasa la salida del primer comando a grep, que deja solo las líneas que contienen el texto project ; entre ellas están las que nombran un módulo del proyecto. Si se cuela algún aviso de Gradle, ignóralo. Debe salir una que empieza por +--- project ':torneo'. Esa línea es la dependencia: :app ya «instaló» :torneo. En el paso siguiente, además, import com.sebastiannarvaez.marcador.torneo.Ejemplo dejará de salir en rojo.

Qué ve :app de :torneo, y qué no. Ve las clases, funciones y propiedades públicas. En Kotlin todo es público salvo que escribas private (o internal, que quiere decir «solo dentro de su módulo»). Y hay una regla que se usará en la lección de El hilo principal y el ANR: implementation no se transmite al código. Las librerías que :torneo instala con implementation (la serialización, las corrutinas) las usa :torneo por dentro. Viajan dentro de la app, porque :torneo las necesita para funcionar, pero el compilador de :app no las ve: no puede usarlas en su código. Si las necesita, las declara ella.

Paso · Sustituye MainActivity por una que pinte la tabla

Objetivo: que MainActivity pinte la tabla del torneo de ejemplo de :torneo. Es la prueba de que la dependencia funciona.

app → kotlin+java → com.sebastiannarvaez.marcador
MainActivity.ktse modifica: su contenido se sustituye
El fichero que se modifica, tal como lo ves en la vista Android: ya no es el esqueleto de la plantilla.
  1. En la vista Android, despliega app → kotlin+java → com.sebastiannarvaez.marcador y haz doble clic en MainActivity.kt. Edítalo donde esté: no crees otro con el mismo nombre.
  2. Selecciona todo el contenido (Cmd+A en Mac, Ctrl+A en Windows y Linux) y bórralo. Escribe el código nuevo.
  3. Los import los puedes escribir a mano o dejar que Android Studio los añada: con el cursor sobre un nombre en rojo, pulsa Option+Enter (Alt+Enter en Windows y Linux) y elige el import. Si te ofrece varios, elige el de androidx.

El código nuevo usa MaterialTheme directamente, así que ya no necesita el tema de la plantilla (MarcadorTheme, en ui.theme). Esos ficheros se quedan donde están, sin usar: no los borres.

MainActivity.kt: versión mínima (con comentarios)
package com.sebastiannarvaez.marcador
 
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.sebastiannarvaez.marcador.torneo.Ejemplo
 
// Tu pantalla. No la construyes tú con `MainActivity()`: la crea Android.
// «: ComponentActivity()» significa «hereda de la Activity de Jetpack».
class MainActivity : ComponentActivity() {
 
    // `override` sobrescribe un método de la clase madre. Este es el callback
    // que Android llama cuando crea la pantalla.
    override fun onCreate(savedInstanceState: Bundle?) {
        // Primero, el trabajo de base de la clase madre.
        super.onCreate(savedInstanceState)
 
        // La tabla de un torneo de ejemplo. `Ejemplo` vive en :torneo.
        val tabla = Ejemplo.torneoConPartidos().tablaDePosiciones()
 
        // Lo que hay entre las llaves es lo que se dibuja en la pantalla.
        setContent {
            MaterialTheme {
                Column(Modifier.padding(24.dp)) {   // apila los elementos, con margen
                    Text("Marcador")
                    tabla.forEach { fila ->         // un Text por cada fila de la tabla
                        Text("${fila.equipo.nombre}: ${fila.puntos} pts")
                    }
                }
            }
        }
    }
}

Piezas nuevas de este paso (Jetpack, Compose, Modifier y dp ya los tienes explicados arriba, antes del ejemplo de la tienda):

  • class MainActivity : ComponentActivity(): tu pantalla, heredando de la Activity que trae Jetpack. override fun onCreate(savedInstanceState: Bundle?): el callback; el Bundle? es un paquete de datos guardados que puede ser null (Nulabilidad).
  • super.onCreate(…): llama al método de la clase madre, que hace su trabajo de base. Va primero; en la lección siguiente verás por qué.
  • setContent { … }: lo que hay entre las llaves es la pantalla. Compose se estudia entera en Compose: composables y modifiers; hoy escribe esa parte tal cual.
  • MaterialTheme { … } y Modifier.padding(24.dp): igual que en la tienda, ahora con 24 dp de margen. tabla.forEach { fila -> … }: repite el bloque para cada fila de la tabla, con la lambda de Lambdas y funciones de orden superior.
  • Ejemplo.torneoConPartidos().tablaDePosiciones(): es de :torneo (Ejemplo lo ampliaste en Dispatchers y withContext); el import com.sebastiannarvaez.marcador.torneo.Ejemplo es la prueba de que :app ve a :torneo.

Paso · Ejecuta Marcador en el emulador, por primera vez

Objetivo: ver Marcador funcionando en el emulador.

Hasta hoy solo habías ejecutado :torneo, que es un programa de consola y corre en tu ordenador. :app es una app Android: para ejecutarla hace falta un dispositivo Android, y el tuyo es el emulador que creaste en Instalar Android Studio y el SDK.

1 · Arranca el emulador. Abre View → Tool Windows → Device Manager. En la lista sale el dispositivo que creaste; púlsale el botón de arrancar de su fila (un triángulo), como hiciste entonces. Espera a ver la pantalla de inicio de Android. Si en cambio prefieres arrancarlo desde la terminal, el Reto de esa lección te enseñó cómo (emulator -list-avds y emulator -avd con el nombre que salga).

2 · Elige dónde y qué ejecutar. En la barra de herramientas de arriba hay dos desplegables junto a los botones Run y Debug:

  • El selector de dispositivo dice el nombre del dispositivo en el que se ejecutará la app. Con el emulador arrancado debe salir el tuyo. Si no sale, ábrelo y elígelo de la lista.
  • El selector de configuración de ejecución dice qué se ejecuta. Una configuración de ejecución es una receta guardada: «ejecuta este módulo de esta forma». Android Studio creó una llamada app para :app, y debe estar elegida. Si ves otra cosa, por ejemplo MainKt (la que quedó al depurar :torneo en Ficheros y depurador), ábrelo y elige app.

3 · Pulsa Run, el triángulo verde. Es la primera vez que se ejecuta :app, y pasan cuatro cosas seguidas:

  1. Gradle compila primero :torneo y luego :app, porque :app depende de él.
  2. Gradle empaqueta la app en un fichero APK (Android Package, el formato en que se instalan las apps de Android).
  3. Android Studio instala ese APK en el emulador, igual que cuando instalas una app desde una tienda.
  4. Android Studio arranca la app: le pide al sistema que abra la pantalla marcada como LAUNCHER en el manifiesto, que es MainActivity. Es el trabajo del intent-filter que acabas de leer.

La primera vez tarda varios minutos, porque Gradle compila todo y puede descargar piezas; las siguientes, segundos. Mientras trabaja, la ventana Run de abajo enseña lo que va haciendo, y al terminar la app se abre sola en el emulador.

4 · Mira el resultado. En la pantalla del emulador debe verse «Marcador» y cuatro líneas con equipo y puntos, en este orden:

Marcador
Rayo FC: 3 pts
Águilas: 3 pts
Lobos: 0 pts
Toros: 0 pts

Es la tabla que calcula :torneo, dibujada por :app: la prueba de que la dependencia funciona.

La app ya está instalada en el emulador. Si pulsas el botón de inicio y abres la lista de aplicaciones (deslizando hacia arriba desde la parte baja de la pantalla), verás una con el nombre Marcador y un icono verde con el robot de Android: es el icono de la plantilla. Sale de mipmap, la carpeta de recursos donde Android guarda los iconos de la app en varios tamaños (la ves en la vista Android; los recursos se explican en Gradle: ficheros, catálogo y dependencias). Tocarlo abre MainActivity de nuevo, exactamente como acaba de hacer Run.

Sin el botón Run. Todo lo que hace Run se puede hacer desde la Terminal de Android Studio, que se abre en la carpeta raíz del proyecto:

./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.sebastiannarvaez.marcador/.MainActivity

El primero compila y empaqueta, el segundo instala (-r reinstala si ya estaba) y el tercero arranca la pantalla .MainActivity (am start -n es «abre esta pantalla por su nombre»); las dos últimas órdenes están en la Chuleta de adb. Si adb responde «command not found», es que falta el paso de PATH de Instalar Android Studio y el SDK (en un Mac, la carpeta suele ser ~/Library/Android/sdk/platform-tools).

Punto de control

  • build.gradle.kts (Module :app) tiene implementation(project(":torneo")) como primera línea de dependencies, y Sync Now termina sin errores rojos.
  • MainActivity.kt se abre sin nada en rojo y ./gradlew :app:assembleDebug termina en BUILD SUCCESSFUL.
  • Run con el selector en app instala Marcador en el emulador y lo abre: «Marcador» y cuatro líneas de equipo y puntos (Rayo FC y Águilas con 3, Lobos y Toros con 0). En la lista de aplicaciones del emulador hay una llamada Marcador.
  • ./gradlew :torneo:test sigue en verde con 137 tests, los mismos que al terminar Tests con backgroundScope y Turbine: no has tocado :torneo.

Comprueba lo que entendiste

7 preguntas · necesitas 5 para completar la lección

  1. 1.¿Quién decide cuándo se crea y cuándo se destruye una Activity?

  2. 2.¿Para qué sirve el bloque <intent-filter> con MAIN y LAUNCHER dentro de <activity> en el Manifest?

  3. 3.Quieres que :app use las clases de :torneo. ¿Dónde escribes implementation(project(":torneo"))?

  4. 4.Pulsas Run por primera vez con el emulador arrancado. ¿Qué ocurre, en orden?

  5. 5.Escribes una OtraActivity en Kotlin pero no la declaras en AndroidManifest.xml. ¿Qué pasa al intentar abrirla?

  6. 6.En un proyecto copiado de internet ves implementation(projects.torneo), pero en tu Marcador da error de referencia sin resolver. ¿Qué escribes?

  7. 7.En el ejemplo de la tienda, TiendaActivity está escrita y declarada en el manifiesto, pero en ningún sitio aparece TiendaActivity(). ¿Quién la crea?

Faltan 7 de 7