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:
<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>| Trozo | Qué 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
importempiezan porandroidx. - 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.Textpinta un texto yColumnapila 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. Undpocupa 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.
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:
<activity android:name=".TiendaActivity" />Si Android abriera esa pantalla, en el móvil se vería esto:
Tienda
· Pan
· Leche
· HuevosPara 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íneainclude(":torneo")desettings.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:
implementation(project(":torneo")) // implementation: entra en el código y en la app; project(":torneo"): el módulo con esa rutaproject(":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.
- Abre
Gradle Scripts → build.gradle.kts (Module :app)con doble clic. - 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.
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.
- 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. - Comprueba. En la Terminal de Android Studio (View → Tool Windows → Terminal) pide a Gradle la lista de lo que usa
:appal ejecutarse en su versióndebug(la que ejecutas mientras desarrollas; lareleasees la que se publica).--configuration debugRuntimeClasspathes 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.
- En la vista Android, despliega
app → kotlin+java → com.sebastiannarvaez.marcadory haz doble clic enMainActivity.kt. Edítalo donde esté: no crees otro con el mismo nombre. - Selecciona todo el contenido (Cmd+A en Mac, Ctrl+A en Windows y Linux) y bórralo. Escribe el código nuevo.
- Los
importlos 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 deandroidx.
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.
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; elBundle?es un paquete de datos guardados que puede sernull(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 { … }yModifier.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(Ejemplolo ampliaste en Dispatchers y withContext); elimport com.sebastiannarvaez.marcador.torneo.Ejemploes la prueba de que:appve 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
apppara:app, y debe estar elegida. Si ves otra cosa, por ejemploMainKt(la que quedó al depurar:torneoen Ficheros y depurador), ábrelo y eligeapp.
3 · Pulsa Run, el triángulo verde. Es la primera vez que se ejecuta :app, y pasan cuatro cosas seguidas:
- Gradle compila primero
:torneoy luego:app, porque:appdepende de él. - Gradle empaqueta la app en un fichero APK (Android Package, el formato en que se instalan las apps de Android).
- Android Studio instala ese APK en el emulador, igual que cuando instalas una app desde una tienda.
- Android Studio arranca la app: le pide al sistema que abra la pantalla marcada como
LAUNCHERen el manifiesto, que esMainActivity. Es el trabajo delintent-filterque 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 ptsEs 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/.MainActivityEl 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)tieneimplementation(project(":torneo"))como primera línea dedependencies, y Sync Now termina sin errores rojos.MainActivity.ktse abre sin nada en rojo y./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL.- Run con el selector en
appinstala 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:testsigue 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.¿Quién decide cuándo se crea y cuándo se destruye una Activity?
2.¿Para qué sirve el bloque
<intent-filter>conMAINyLAUNCHERdentro de<activity>en el Manifest?3.Quieres que
:appuse las clases de:torneo. ¿Dónde escribesimplementation(project(":torneo"))?4.Pulsas Run por primera vez con el emulador arrancado. ¿Qué ocurre, en orden?
5.Escribes una
OtraActivityen Kotlin pero no la declaras enAndroidManifest.xml. ¿Qué pasa al intentar abrirla?6.En un proyecto copiado de internet ves
implementation(projects.torneo), pero en tu Marcador da error de referencia sin resolver. ¿Qué escribes?7.En el ejemplo de la tienda,
TiendaActivityestá escrita y declarada en el manifiesto, pero en ningún sitio apareceTiendaActivity(). ¿Quién la crea?