Intents explícitos y una segunda pantalla
Al terminar: Abrir otra pantalla de tu app con un intent explícito, pasarle datos con extras y cerrar con finish.
Al terminar esta lección, Marcador tendrá una segunda pantalla, DetalleActivity, que se abre desde un botón de la principal, recibe un texto (Rayo FC 2-1 Toros) y se cierra con otro botón, «Volver».
Importa porque un Intent es la forma que tiene Android de pedir cosas: abrir una pantalla, compartir un texto, abrir un enlace. Hoy aprendes la forma más sencilla, el intent explícito, para moverte dentro de tu app. En la lección siguiente, Intents implícitos y filtros, pides cosas a otras apps.
Un intent es un mensaje
Un Intent es un mensaje que describe qué se quiere hacer. Se lo entregas a Android con startActivity(intent) y el sistema lo entrega a quien toque. Puede traer:
- Qué hacer: una acción (
action, comoACTION_SEND, «enviar», oACTION_VIEW, «ver») y, a veces, una dirección (data, una URI: una dirección con forma de enlace, comohttps://kotlinlang.org). Esas dos las usas en la lección siguiente. - Datos sueltos, los extras: pares (clave, valor) que viajan con el mensaje. Son lo que usas hoy.
Hay dos formas de dirigirlo:
| Explícito | Implícito | |
|---|---|---|
| Qué dices | Quién lo hace: una clase concreta | Qué quieres hacer |
| Quién lo resuelve | Nadie: el destino ya está nombrado | El sistema, entre las apps que declaran saberlo |
| Se usa para | Moverte dentro de tu app | Pedir algo a otras apps (compartir, abrir una web) |
| Dónde se ve | Hoy | La lección siguiente |
Un componente es cada pieza de una app que Android sabe arrancar: una Activity (una pantalla), un servicio (trabajo sin pantalla) o un receptor de difusión (BroadcastReceiver: escucha avisos del sistema). Un ComponentName es simplemente el nombre completo de ese componente (paquete y clase). Hoy solo usas Activities.
val explicito = Intent(
this, // la Activity desde la que abres: es un Context
DetalleActivity::class.java // la clase destino, en el formato que pide Android (se explica abajo)
)
startActivity(explicito) // entrega el mensaje a AndroidLas piezas, en una app de recetas
Las piezas que vas a usar:
Clase::class.java:Clase::classes la referencia a la clase misma, no a un objeto de ella (::apunta a algo sin ejecutarlo, como en Lambdas con listas: filter, map y reglas), y.javala convierte en el tipo que Android espera (la clase de Java, el lenguaje original de Android); no hay más que saber.Intent(contexto, Clase::class.java)es el intent explícito..putExtra(clave, valor)añade un extra al intent yintent.getStringExtra(clave)lo lee en la otra pantalla; devuelvenullsi no existe, por eso lo acompañas de?:(el «si no hay, esto» de Nulabilidad).intent(con minúscula, sin escribirlo tú): todaActivitytrae una propiedadintentcon elIntentque la abrió. Por esoDetalleActivitypuede leerintent.getStringExtra(…)dentro deonCreatesin que nadie se lo pase: es el mensaje que Android entregó al crearla.companion object(en object, companion y vararg): aquí guarda la clave del extra. La clave lleva el paquete delante para no chocar con las de otras apps ni librerías.finish(): cierra la Activity actual y vuelves a la anterior. Android apila las pantallas que abres (la pila de Activities, o back stack): abrir una añade una encima,finish()o el botón Atrás la quita.android:exporteden el Manifest: dice si otras apps pueden lanzar ese componente. Con"false", solo tu app puede abrirDetalleActivity. Con un intent explícito dentro de tu app,falsees lo correcto.
Todas esas piezas, en una app de recetas inventada. No es Marcador: los nombres y los datos son otros, pero la forma es la que vas a escribir.
// En la pantalla que ABRE otra (intent explícito con un extra):
val abrir = Intent(this, RecetaActivity::class.java) // destino: una clase concreta de tu app
.putExtra(RecetaActivity.EXTRA_PLATO, "Tortilla") // un extra: clave y valor que viajan con el mensaje
startActivity(abrir) // Android crea RecetaActivity y la pone encima
// En la pantalla que SE ABRE:
class RecetaActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val plato = intent.getStringExtra(EXTRA_PLATO) ?: "Sin plato" // el extra, o un texto por defecto
setContent {
Column {
Text("Plato: $plato")
Button(onClick = { finish() }) { Text("Volver") } // cierra esta pantalla: vuelves a la anterior
}
}
}
companion object {
const val EXTRA_PLATO = "com.ejemplo.recetas.PLATO" // la clave, con el paquete delante para no chocar
}
}Y su línea en el manifiesto. Los comentarios van fuera de las etiquetas, porque XML no los admite dentro:
<!-- Va dentro de <application>, junto a las demás <activity>. -->
<!-- exported="false": solo esta app puede abrirla. -->
<activity
android:name=".RecetaActivity"
android:exported="false" />En ese ejemplo, si RecetaActivity se abriera sin ningún extra, la pantalla diría Plato: Sin plato, porque getStringExtra devuelve null y el ?: pone el texto por defecto. Al pulsar «Volver», finish() saca la pantalla de la pila y se destruye: ves la que la abrió.
Ejercicio · una pantalla que se abre a sí misma
Reto · Ejercicio: la pila de pantallas
Practicas el intent explícito, los extras y la pila de Activities sin tocar el manifiesto: la propia pantalla principal se abre a sí misma, una copia encima de otra.
Dónde. En la vista Android, clic derecho sobre com.sebastiannarvaez.marcador → New → Package, y escribe .ejercicios.pila_de_pantallas (la vista Android pega ese nombre al paquete). Después, clic derecho sobre la carpeta nueva → New → Kotlin Class/File, elige File y llámalo PilaDePantallas. La carpeta ejercicios es la que creaste en Rotación y muerte del proceso. Esa carpeta se queda: los ejercicios no se borran.
Qué hacer. Escribe una función, apilarCopias, que reciba la pantalla (de tipo ComponentActivity) y su sobre guardado (Bundle?). Si hay sobre, la pantalla se está recreando por un giro y la función no hace nada. Si no, lee del intent que la abrió un número entero de «copia» (cero si no viene). Si es menor que 2, escribe en Logcat, con el tag Ejercicio, «copia N: abro la copia N+1» y abre otra pantalla principal encima, con un intent explícito que lleva N+1 como extra. Si no es menor que 2, escribe «copia 2: soy la última».
Cómo se prueba. Añade en MainActivity.kt, dentro de onCreate, justo debajo de anotar("onCreate …"), esta llamada temporal, con la ruta completa para no necesitar import (this es la pantalla y savedInstanceState ya existe en onCreate). Termina en un comentario para que la reconozcas al borrarla:
com.sebastiannarvaez.marcador.ejercicios.pila_de_pantallas.apilarCopias(this, savedInstanceState) // EJERCICIO: quitar al terminarSi MainActivity sale en rojo, pulsa Option+Enter (Alt+Enter en Windows y Linux) sobre el error y acepta el import. Ejecuta con Run y mira Logcat con tag:Ejercicio. Debes ver:
D Ejercicio: copia 0: abro la copia 1
D Ejercicio: copia 1: abro la copia 2
D Ejercicio: copia 2: soy la últimaEn tag:Ciclo salen tres onCreate con tres números distintos tras la arroba: son tres pantallas apiladas. Pulsa Atrás una vez: la copia 2 termina con onDestroy (isFinishing=true, …) y ves la copia 1, que vuelve con onRestart y sin línea nueva de Ejercicio. Pulsa Atrás otra vez y queda la copia 0.
Pistas. Todo lo que necesitas ya lo has visto: Intent(this, Clase::class.java) y putExtra (arriba), la propiedad intent, const val para la clave con el paquete delante. Una pieza nueva: getIntExtra(clave, valorPorDefecto) lee un entero; como un Int no puede faltar, se le da el valor para cuando no viene, en vez de usar ?:.
Ver solución
package com.sebastiannarvaez.marcador.ejercicios.pila_de_pantallas
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import com.sebastiannarvaez.marcador.MainActivity
// La clave del extra, con el paquete delante para no chocar con otras.
const val EXTRA_COPIA = "com.sebastiannarvaez.marcador.COPIA"
// Cada copia de la pantalla lee su número y, si es menor que 2, abre la siguiente encima.
fun apilarCopias(pantalla: ComponentActivity, sobre: Bundle?) {
if (sobre != null) return // es una recreación (un giro): no abre nada
val copia = pantalla.intent.getIntExtra(EXTRA_COPIA, 0) // 0 si el intent no trae el extra
if (copia < 2) {
Log.d("Ejercicio", "copia $copia: abro la copia ${copia + 1}")
val siguiente = Intent(pantalla, MainActivity::class.java) // intent explícito: la misma clase
.putExtra(EXTRA_COPIA, copia + 1)
pantalla.startActivity(siguiente)
} else {
Log.d("Ejercicio", "copia $copia: soy la última")
}
}La comprobación del sobre evita un lío. Sin ella, si giras con las copias abiertas, al volver atrás cada copia se recrea, pasa otra vez por onCreate y vuelve a abrir la siguiente.
Antes del paso, borra la línea temporal (la que termina en // EJERCICIO: quitar al terminar) y comprueba que MainActivity.kt vuelve a estar limpio: Cmd+F (Ctrl+F en Windows y Linux) con «ejercicios» no debe dar ningún resultado. Si Android Studio añadió un import, bórralo también.
Paso · La segunda pantalla, DetalleActivity
Objetivo. Añadir DetalleActivity y un botón en MainActivity que la abre con un intent explícito, pasándole el resumen del partido como extra. Al terminar: «Ver detalle» abre la pantalla con Rayo FC 2-1 Toros, y «Volver» regresa.
Los dos ficheros nuevos van cada uno en su carpeta. Primero, las carpetas: en el panel, app → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador (el que no lleva (test) ni (androidTest)) → New → Package, añade al final .core.domain y pulsa Intro. laboratorio ya existe desde Fugas de memoria, así que no hay que crearla otra vez. Mientras core tenga una sola subcarpeta, la vista Android las enseña juntas en una fila, core.domain; en cuanto tenga dos, las separa. Después, clic derecho sobre core.domain → New → Kotlin Class/File → File, escribe Resumen y pulsa Intro; y lo mismo sobre laboratorio (la de Fugas de memoria) con DetalleActivity. Android Studio crea cada fichero con su línea package y lo abre. La clase DetalleActivity la escribes tú dentro.
El proyecto no usa nada que no hayas instalado ya: Intent viene con el propio Android, y la pantalla usa Compose, que ya está en :app. No hay que tocar Gradle.
Qué hay que hacer en cada fichero (obligatorio; el código completo está más abajo):
Resumen.kt: la funciónPartido.resumen(), que da el texto de un partido.DetalleActivity.kt: la pantalla, que lee su texto de un extra.MainActivity.kt: una función privadaabrirDetalle()dentro de la clase (justo encima decalcularBloqueando) y un botón en laColumnque ya tienes, trasText("Marcador").import android.content.Intentya está arriba deMainActivity.ktdesde El ciclo de vida de la Activity (lo usaonNewIntent); siIntentsale en rojo, con el cursor sobre el nombre pulsa Option+Enter y elige el import.Ejemploes el de:torneo, también ya importado.AndroidManifest.xml: ábrelo enapp → manifests → AndroidManifest.xml. La clase ya no está en la raíz, así que su nombre lleva la carpeta:.laboratorio.DetalleActivity. Una segunda<activity>dentro de<application>, justo antes de la línea</application>, es decir, después de la deMainActivity.
Ahora, el paso en tu proyecto. Los títulos de los bloques de abajo son las rutas de los ficheros en tu proyecto; en la vista Android están en app → kotlin+java (o app → manifests para el manifiesto).
Intenta escribirlo tú, en este orden:
Resumen.kt: una función,resumen, que da el texto de un partido con el nombre del local, el marcador y el del visitante. Resultado esperado: para el primer partido de ejemplo,Rayo FC 2-1 Toros.DetalleActivity, una segunda pantalla con el título «Detalle del partido», el resumen que recibe en un extra y un botón «Volver». Si el extra no llega, dice «Sin partido».- En
MainActivity, un botón «Ver detalle (intent explícito)» que abre el detalle con el resumen del primer partido. En el Manifest, la<activity>deDetalleActivity, no exportada.
Lo que debes ver: el detalle con Rayo FC 2-1 Toros al pulsar el botón.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.core.domain
import com.sebastiannarvaez.marcador.torneo.Partido
import com.sebastiannarvaez.marcador.torneo.marcador
// «Rayo FC 2-1 Toros»: el texto que se muestra y se comparte.
fun Partido.resumen(): String = "${local.nombre} ${marcador()} ${visitante.nombre}"package com.sebastiannarvaez.marcador.laboratorio
import android.os.Bundle
import android.util.Log
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.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
// Segunda pantalla. Se abre con un intent EXPLICITO desde MainActivity: el
// resumen del partido viaja en un extra (`EXTRA_RESUMEN`).
class DetalleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// `intent` es el mensaje con el que Android abrio esta pantalla.
val texto = intent.getStringExtra(EXTRA_RESUMEN) ?: "Sin partido"
Log.d("Intents", "DetalleActivity abierta con action=${intent.action} data=${intent.data} -> $texto")
setContent {
MaterialTheme {
Column(Modifier.padding(24.dp)) {
Text("Detalle del partido")
Text(texto)
// finish() saca esta pantalla de la pila: vuelves a la anterior.
Button(onClick = { finish() }) { Text("Volver") }
}
}
}
}
companion object {
// Convencion: prefijar la clave con el paquete, para no chocar con otras.
const val EXTRA_RESUMEN = "com.sebastiannarvaez.marcador.RESUMEN"
}
}En MainActivity.kt, esta función va dentro de la clase. Como DetalleActivity y resumen viven ahora en laboratorio y core.domain, arriba del fichero hacen falta import com.sebastiannarvaez.marcador.laboratorio.DetalleActivity e import com.sebastiannarvaez.marcador.core.domain.resumen (Option+Enter los añade). El resto del fichero no cambia.
// Intent EXPLICITO: el destino es una clase tuya, sin ambiguedad. Los datos
// viajan como extras (clave, valor).
private fun abrirDetalle() {
val intent = Intent(this, DetalleActivity::class.java)
.putExtra(DetalleActivity.EXTRA_RESUMEN, Ejemplo.rayoContraToros.resumen())
startActivity(intent)
}Y el botón va dentro de la Column, tras Text("Marcador").
Button(onClick = { abrirDetalle() }) { Text("Ver detalle (intent explícito)") }En AndroidManifest.xml, esta <activity> va justo antes de </application>. El resto del fichero no cambia.
<!-- Segunda pantalla. exported="false": solo esta app puede abrirla. -->
<activity
android:name=".laboratorio.DetalleActivity"
android:exported="false" />Probar el intent explícito
Ejecuta la app con Run, con app elegido arriba (el emulador se arranca como en La Activity). Antes de la prueba limpia Logcat con adb logcat -c en la Terminal de Android Studio; los comandos adb están en la Chuleta de adb (si adb no se reconoce, mira Instalar Android Studio y el SDK).
Pulsa Ver detalle (intent explícito) y lee Logcat con adb logcat -d -s Intents. Verás algo como esto:
D Intents : DetalleActivity abierta con action=null data=null -> Rayo FC 2-1 TorosSin action ni data: el texto llegó por el extra. Pulsa Volver y estás otra vez en la principal.
En la lección siguiente, Intents implícitos y filtros, esta misma pantalla aprende a recibir enlaces y a compartir.
Punto de control
MainActivity.ktno contiene la palabraejercicios(la línea temporal del ejercicio ya está borrada).- Ver detalle abre la segunda pantalla con
Rayo FC 2-1 Toros, y Volver regresa a la principal. - En la Terminal de Android Studio,
./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Qué distingue a un intent explícito de uno implícito?
2.Para pasar el texto
Rayo FC 2-1 TorosaDetalleActivitydesde un botón, dentro de tu app, ¿qué usas?3.Pulsas «Volver» en
DetalleActivity, que llama afinish(). ¿Qué ocurre?4.
DetalleActivityhaceintent.getStringExtra(EXTRA_RESUMEN) ?: "Sin partido"y se abre sin ningún extra. ¿Qué muestra?