Deep links en Android
Al terminar: Abrir el Pokémon correcto desde un enlace, con la app cerrada y con la app abierta.
Al terminar, un enlace como pokedex://pokemon/25 abre la ficha de Pikachu en tu app. Con la app cerrada y con la app ya abierta: son dos casos distintos por dentro, y el segundo es el que casi nadie prueba.
También verás por qué el enlace https no abre la app por sí solo en este proyecto, aunque el manifiesto lo pida. Es un límite real de la web en la que vive la página del curso, se comprueba con una orden de adb, y es la razón de que el esquema propio (pokedex://) sea el camino principal. Y verás cómo aprobar ese dominio a mano para ver el https abriendo la app.
El concepto · Un enlace que abre una pantalla
Un enlace profundo (en inglés, deep link) es una dirección que, en vez de abrir una página web, abre tu app en una pantalla concreta. La dirección tiene tres partes: el esquema (lo que va antes de ://, como https o pokedex), el dominio (el nombre de la web, como ejemplo.org) y la ruta (lo que va detrás, como /pokemon/25). Es como una dirección postal: el esquema es el tipo de envío (carta o paquete), el dominio es la ciudad y la calle, y la ruta es la puerta concreta.
Un enlace a un Pokémon se puede escribir de dos maneras, y ninguna sirve para todo:
pokedex://pokemon/25(esquema propio) funciona siempre, sin dominio y sin verificación. A cambio, la mayoría de apps de mensajería no lo convierten en enlace pulsable: se queda en texto.https://sebastiannarvaez11.github.io/pokedex-kmp/pokemon/25sí es pulsable en cualquier sitio. A cambio, el sistema exige comprobar que ese dominio es tuyo. Un enlacehttpscuyo dominio Android ha verificado así, y que por eso abre la app directamente, se llama App Link (enlace de aplicación).
Para que la app entienda las dos, hay que leer sus partes y quedarse con lo que importa. Este ejemplo, ajeno al proyecto, hace justo eso con una biblioteca imaginaria que publica su web en una subcarpeta (/catalogo):
// Un enlace tiene tres partes: esquema://dominio/ruta
// Una biblioteca imaginaria publica su web en una subcarpeta, /catalogo.
const val ESQUEMA = "biblio"
const val DOMINIO = "ejemplo.org"
const val PREFIJO = "/catalogo"
// Devuelve el número del libro, o null si el enlace no es de un libro.
fun libroDe(url: String): Int? {
val resto = url
.substringAfter("://") // quita «biblio://» o «https://»
.let { if (it.startsWith(DOMINIO)) it.removePrefix(DOMINIO) else it } // quita el dominio, si está (`let` recibe el texto como `it` y devuelve lo que calcules)
.let { if (it.startsWith(PREFIJO)) it.removePrefix(PREFIJO) else it } // y la subcarpeta, si está
val ruta = "/" + resto.trim('/') // «/libro/42»
// `toIntOrNull()` da el número, o `null` si el texto no es un número.
return if (ruta.startsWith("/libro/")) ruta.removePrefix("/libro/").toIntOrNull() else null
}
fun main() {
println(libroDe("biblio://libro/42")) // esquema propio
println(libroDe("https://ejemplo.org/catalogo/libro/42")) // https con subcarpeta
println(libroDe("https://ejemplo.org/catalogo/libro/quijote")) // no es un número
println(libroDe("biblio://socios/7")) // otra pantalla que no existe
}42
42
null
nullPégalo en el Playground de Kotlin (play.kotlinlang.org) y ejecútalo. Es lo que hará Destination.parse en el paso 1, con el dominio y la subcarpeta de la web del curso. Fíjate en los dos null: un enlace que no entendemos no rompe nada, simplemente no lleva a ninguna parte.
Paso 1 · Destination entiende el dominio del curso
El archivo. Destination.kt (shared/src/commonMain/kotlin/com/sebastiannarvaez/pokedex/navigation/Destination.kt, el del mapa de pantallas) ya sabe leer pokedex://pokemon/25 y un https con el dominio de ejemplo pokedex-kmp.example. Lo que falta es apuntarlo a una página que exista. Usamos la web que el curso tiene publicada, sebastiannarvaez11.github.io/pokedex-kmp: una página gratuita que vive en una subcarpeta (/pokedex-kmp) de un dominio que no es del curso. No es tuya, y no hace falta que lo sea para esta lección; en una app propia pondrías aquí tu dominio.
La página del curso vive bajo esa subcarpeta y no en la raíz del dominio. Por eso el enlace lleva un prefijo (/pokedex-kmp) que parse tiene que saltarse, igual que PREFIJO en el ejemplo. Sustituye el companion object entero (es lo que está dentro de la interfaz Destination, tras toPath) por este:
companion object {
/** El esquema propio, que funciona sin dominio ni verificación. */
const val ESQUEMA = "pokedex"
/** El dominio del enlace `https`, que sí se puede compartir por chat. */
const val HOST = "sebastiannarvaez11.github.io" // antes: pokedex-kmp.example
/** El prefijo de la ruta: la subcarpeta donde vive la web del curso. */
const val BASE_PATH = "/pokedex-kmp" // nuevo
/**
* Traduce una URL entrante a una pantalla.
*
* Devuelve null en vez de lanzar: una URL que no entendemos no es un
* error de programación, es alguien pegando cualquier cosa.
*/
fun parse(url: String): Destination? {
val sinEsquema = url
// Quita «pokedex://» o «https://». Si el texto no trae «://»,
// `missingDelimiterValue = url` lo deja entero, tal cual.
.substringAfter("://", missingDelimiterValue = url)
// Si empieza por el dominio, lo quita.
.let { if (it.startsWith(HOST)) it.removePrefix(HOST) else it }
// Nuevo: y si después viene el prefijo del proyecto, también.
.let { if (it.startsWith(BASE_PATH)) it.removePrefix(BASE_PATH) else it }
// `substringBefore('?')` descarta lo que va detrás de un «?»: los parámetros
// que algunos enlaces añaden (`…/pokemon/25?origen=chat`) y que aquí no usamos.
val ruta = "/" + sinEsquema.trim('/').substringBefore('?')
return when {
ruta == "/" -> Lista
ruta.startsWith("/pokemon/") ->
// `toIntOrNull()` da el número, o `null` si el texto no lo es.
// `?.let(::Detalle)` solo actúa si hay número: `::Detalle` es el
// constructor de `Detalle` pasado como función, igual que
// `{ n -> Detalle(n) }`.
ruta.removePrefix("/pokemon/").toIntOrNull()?.let(::Detalle)
else -> null
}
}
}Y al final del fichero, toShareUrl() (la que construye el enlace https) tiene que escribir el prefijo; toDeepLink() no cambia:
/** El enlace que se comparte: `https`, para que sea pulsable en cualquier app. */
fun Destination.toShareUrl(): String =
"https://${Destination.HOST}${Destination.BASE_PATH}${toPath()}"Las dos direcciones salen del mismo sitio para que no se separen nunca. Un test de El mapa de pantallas compartido escribe el dominio de ejemplo a mano en dos sitios, así que hay que actualizarlo. Abre shared/src/commonTest/kotlin/com/sebastiannarvaez/pokedex/navigation/DestinationTest.kt y sustituye estas dos funciones:
@Test
fun elEnlaceHttpsAbreLaMismaPantalla() {
// Las dos formas llevan al mismo sitio: una sola traducción, no dos.
assertEquals(
Destination.parse("pokedex://pokemon/25"),
Destination.parse("https://sebastiannarvaez11.github.io/pokedex-kmp/pokemon/25"),
)
}
@Test
fun laRaizEsLaLista() {
assertEquals(Destination.Lista, Destination.parse("pokedex://"))
assertEquals(Destination.Lista, Destination.parse("https://sebastiannarvaez11.github.io/pokedex-kmp/"))
}En la Terminal de Android Studio, en la carpeta de tu proyecto:
./gradlew :shared:jvmTestDebe terminar con BUILD SUCCESSFUL y ningún test en rojo. Son los mismos 76 tests de la JVM que antes (si quieres ver la cifra, abre el informe de tests como ya hiciste antes; el triángulo verde junto a la clase solo ejecuta los de esa clase, los cinco de DestinationTest): esta lección cambia dos tests, no añade ninguno. Si falla elEnlaceHttpsAbreLaMismaPantalla, te has dejado el dominio de ejemplo en algún sitio.
Paso 2 · Dos intent-filter en el manifiesto
El archivo. AndroidManifest.xml ya lo has tocado (HTTP y Ktor: el cliente de PokeAPI). Ábrelo en androidApp/src/main/AndroidManifest.xml. La MainActivity ya tiene el filtro de lanzador, el que hace que el icono abra la app. En su elemento <activity> hay dos cambios, y conservas todos los atributos que ya tiene:
- Un atributo nuevo, junto a los demás (por ejemplo, debajo de
android:label):
android:launchMode="singleTop"launchMode="singleTop" dice «si la pantalla que se pide ya está arriba, reutilízala en vez de crear otra MainActivity». Ojo, que hay dos pilas distintas: esta es la pila de Activities de Android (las pantallas del sistema, una por MainActivity abierta); la lista de claves de Navigation 3 que verás en el paso 4 es otra, dentro de la misma Activity. singleTop es lo que hace que el paso 3 tenga sentido, y lo verás actuar en el paso 6: el segundo am start dirá intent has been delivered to currently running top-most instance.
- Dos filtros nuevos, debajo del
<intent-filter>del lanzador y dentro de la misma<activity>. Un detalle del segundo:android:pathPrefixdice «acepta las rutas que empiecen por esto». Aquí es/pokedex-kmp/pokemon/, así que el filtro solo captura enlaces a fichas; la raíz de la web (la lista) no abre la app. Por esoparseentiende la lista, pero este filtro no la deja llegar:
<!-- Filtro 1: el esquema propio, pokedex://. Funciona siempre, sin
dominio ni verificación. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="pokedex" />
</intent-filter>
<!-- Filtro 2: el enlace https. `autoVerify` pide a Android que
compruebe que el dominio es de la app (paso 5). -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="sebastiannarvaez11.github.io"
android:pathPrefix="/pokedex-kmp/pokemon/" />
</intent-filter>Paso 3 · Recibirlo: dos sitios, no uno
El archivo. MainActivity.kt es la puerta de la app, y el enlace puede llegar por dos puertas distintas: con la app cerrada, dentro del Intent que la arranca (onCreate); con la app ya abierta, en onNewIntent. Hay que atender las dos. Y el destino que llega tiene que ser un estado (mutableStateOf), no un valor leído una vez: así Compose reacciona cada vez que cambia. Sustituye el fichero androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/MainActivity.kt entero por este (PokedexApp aún no acepta esos dos parámetros: Android Studio los marcará en rojo hasta el paso 4). Cuatro cosas nuevas antes de leerlo:
- Hasta ahora
mutableStateOfiba siempre dentro deremember, en un composable. Aquí es una propiedad de la Activity: vive lo que vive ella, y Compose redibuja igual cuando cambia, porquesetContentla lee. intentes una propiedad que tiene toda Activity: el mensaje que la arrancó (en Java se ve comogetIntent(), el mismo nombre con otro aspecto).intent?.dataStringes la dirección que trae ese mensaje, como texto (onullsi no trae ninguna, que es lo que pasa al abrir la app con el icono).Destination::parsepasa la funciónparsecomo valor, igual que::Detalleen el paso 1.- Al girar el móvil,
intentno cambia: sigue siendo el mensaje que arrancó la app, con el enlace dentro. Si lo leyéramos en cadaonCreate, después de volver a la lista y girar el móvil la ficha del enlace volvería a abrirse sola. Por eso el enlace se lee solo la primera vez, cuandosavedInstanceStateesnull. - El resto (
settingsVm, el tema) ya lo tenías de Los ajustes mandan en toda la app, en Android.
package com.sebastiannarvaez.pokedex.android
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sebastiannarvaez.pokedex.android.navigation.PokedexApp
import com.sebastiannarvaez.pokedex.android.ui.PokedexTheme
import com.sebastiannarvaez.pokedex.feature.settings.SettingsViewModel
import com.sebastiannarvaez.pokedex.navigation.Destination
import org.koin.compose.viewmodel.koinViewModel
class MainActivity : ComponentActivity() {
// El destino que trajo el enlace, o null si no hubo. Es un estado
// observable porque puede cambiar con la app ya abierta. Sin `remember`:
// `remember` es solo para composables; aquí es una propiedad de la Activity
// y se crea una vez por cada Activity.
private val destinoEntrante = mutableStateOf<Destination?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// App cerrada: el enlace viene en el Intent que la arrancó.
// `dataString` es la URL; `parse` la traduce o devuelve null.
// Solo si la Activity es nueva: al girar el móvil Android la recrea,
// `savedInstanceState` ya no es null y `intent` sigue siendo el
// original, con el enlace. Sin el `if`, la ficha reaparecería sola.
if (savedInstanceState == null) {
destinoEntrante.value = intent?.dataString?.let(Destination::parse)
}
setContent {
// El tema se lee en la raíz, no dentro de la pantalla de ajustes:
// si no, cambiarlo solo repintaría esa pantalla.
val settingsVm: SettingsViewModel = koinViewModel()
val preferencias by settingsVm.settings.collectAsStateWithLifecycle()
PokedexTheme(tema = preferencias.tema) {
PokedexApp(
destinoEntrante = destinoEntrante.value,
// Cuando la app usa el destino, lo apaga para no repetirlo.
alConsumirDestino = { destinoEntrante.value = null },
)
}
}
}
// App abierta: no hay onCreate, llega un Intent nuevo.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Se lee el parámetro `intent`, no `getIntent()`: ese sigue
// devolviendo el intent original que arrancó la Activity.
destinoEntrante.value = intent.dataString?.let(Destination::parse)
}
}Paso 4 · Dónde cae el enlace dentro de la pila
Primero, la traducción. Navigation 3 navega con claves (ListaKey, DetalleKey), no con Destination. Navegación en Android con Navigation 3 dejó la traducción pendiente para esta lección. Abre androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/navigation/NavKeys.kt, añade el import en su sitio, entre los demás, y la función al final del fichero:
import com.sebastiannarvaez.pokedex.navigation.Destination/** Traduce el mapa compartido a la clave que entiende Navigation 3. */
fun Destination.aNavKey(): NavKey = when (this) {
Destination.Lista -> ListaKey
is Destination.Detalle -> DetalleKey(pokemonId)
}Aquí el when es exhaustivo sobre una interfaz sellada: si mañana existe una pantalla nueva en Destination, esto deja de compilar hasta que decidas su clave.
Después, la pila de Navigation 3 (la lista de claves de dentro de la Activity, no la pila de Activities del paso 2). Abrir una ficha desde fuera no es lo mismo que abrirla desde la lista: si el enlace deja la ficha sola en la pila, el botón de volver cierra la app. Abre androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/navigation/PokedexApp.kt. Solo cambia el principio de la función. Dos import nuevos, junto a los demás (Option+Enter sobre los nombres en rojo también los añade):
import androidx.compose.runtime.LaunchedEffect
import com.sebastiannarvaez.pokedex.navigation.DestinationAntes, el principio de la función era:
@Composable
fun PokedexApp(inicio: NavKey = ListaKey) {
val pila = rememberNavBackStack(inicio)
val actual = pila.lastOrNull()
// El número de la pestaña: otro lector del mismo repositorio de favoritos.
val favoritos: FavoritesViewModel = koinViewModel()
val insignia by favoritos.insignia.collectAsStateWithLifecycle()Ya no hay parámetro inicio: nadie lo usaba (si alguna vista previa se lo pasaba, quítaselo). Después, con el destino entrante. Tres cosas se quedan como estaban:
Scaffold(y todo lo de debajo, exactamente igual.entryDecorators, con sus dos decoradores.- Las tres líneas de la insignia de Favoritos, debajo de
val actual: ya las tenías de la lección de ajustes, no las borres.
@Composable
fun PokedexApp(
destinoEntrante: Destination? = null, // nuevo: el enlace que llegó, si llegó
alConsumirDestino: () -> Unit = {}, // nuevo: avisa de que ya se usó
) {
val pila = rememberNavBackStack(ListaKey) // antes recibía `inicio`; ya no lo usa nadie
val actual = pila.lastOrNull()
// El número de la pestaña: otro lector del mismo repositorio de favoritos.
val favoritos: FavoritesViewModel = koinViewModel()
val insignia by favoritos.insignia.collectAsStateWithLifecycle()
// Se ejecuta al empezar y cada vez que llega un destino distinto.
LaunchedEffect(destinoEntrante) {
val destino = destinoEntrante ?: return@LaunchedEffect // sin enlace, sale del efecto
val clave = destino.aNavKey()
pila.clear() // se parte de cero…
if (clave != ListaKey) pila.add(ListaKey) // …con la lista debajo…
pila.add(clave) // …y el destino encima
alConsumirDestino() // lo gastado se apaga
}return@LaunchedEffect sale solo del bloque del efecto (un return a secas intentaría salir de la función PokedexApp entera). Y ?: dice «si lo de la izquierda es null, haz lo de la derecha».
alConsumirDestino() apaga el estado en cuanto se usa. Para ver por qué, un ejemplo ajeno al proyecto: un buzón que solo avisa cuando su contenido cambia, igual que un LaunchedEffect con clave solo se repite si la clave cambia.
// Un buzón que solo avisa cuando su contenido CAMBIA,
// igual que un LaunchedEffect con clave solo se repite si la clave cambia.
class Buzon(private val consumir: Boolean) {
private var carta: String? = null
fun llega(nueva: String) {
if (nueva == carta) { // mismo valor: no hay cambio, nadie se entera
println("llega $nueva -> (nada)")
return
}
carta = nueva
println("llega $nueva -> abro $nueva")
if (consumir) carta = null // la leo y vacío el buzón
}
}
fun main() {
println("Sin vaciar el buzón:")
val a = Buzon(consumir = false)
a.llega("ficha 25")
a.llega("ficha 25")
println("Vaciándolo al leer:")
val b = Buzon(consumir = true)
b.llega("ficha 25")
b.llega("ficha 25")
}Sin vaciar el buzón:
llega ficha 25 -> abro ficha 25
llega ficha 25 -> (nada)
Vaciándolo al leer:
llega ficha 25 -> abro ficha 25
llega ficha 25 -> abro ficha 25Eso es lo que pasa aquí: se apaga para que el mismo enlace pueda llegar dos veces. Si el estado se quedara en Detalle(25), un segundo enlace al 25 (tras volver a la lista, por ejemplo) pondría el mismo valor, la clave no cambiaría y el efecto no se ejecutaría. Apagado a null, cada enlace es un cambio.
Paso 5 · El App Link que no verifica
autoVerify="true" le pide a Android que compruebe que la app y el dominio son del mismo dueño. La comprobación descarga un fichero, assetlinks.json, que el dueño del dominio publica con la huella SHA-256 del certificado de firma de la app. Toda app de Android va firmada: lleva un sello digital que demuestra quién la hizo. Las de prueba las firma Android Studio solo, con un certificado de depuración que creó en tu ordenador la primera vez. La huella es un resumen corto de ese sello, calculado con un método llamado SHA-256: 32 pares de cifras y letras (del 0 al 9 y de la A a la F, lo que se llama hexadecimal) separados por dos puntos, como 90:1F:FE:…. En el paso 6 verás la de tu app, y será distinta de la del curso porque tu certificado es otro. Así sería el fichero (con com.sebastiannarvaez.pokedex, o el applicationId de tu proyecto, en package_name):
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.sebastiannarvaez.pokedex",
"sha256_cert_fingerprints": ["LA:HUELLA:DE:TU:CERTIFICADO"]
}
}
]Tú no puedes publicarlo en sebastiannarvaez11.github.io: el dominio no es tuyo. Pero aunque lo fuera, aquí hay una segunda trampa. Antes, una palabra: .well-known es una carpeta de nombre fijo que Android y iOS buscan siempre en la raíz de una web, es decir, justo detrás del dominio (https://dominio/.well-known/…) y nunca dentro de una subcarpeta.
Para verificar un App Link hace falta un dominio cuya raíz controles, y que el fichero lleve la huella de tu certificado. Ninguna de las dos cosas está a tu alcance en este curso sin comprar un dominio, y por eso el esquema propio es el camino principal. El https se queda como el enlace que se comparte: sin verificar, abre el navegador en la página del curso que invita a abrir la app.
Paso 6 · Probarlo sin mandarte mensajes
Con el emulador arrancado y la app instalada (botón Run, como en Ejecutar en Android), lo más cómodo es disparar los enlaces desde la Terminal de Android Studio. adb shell ejecuta una orden dentro del emulador; am start (activity manager, el gestor de actividades) lanza una pantalla con el intent que le describas, y pm (package manager, el gestor de paquetes) pregunta por las apps instaladas. am lo usaste en Intents implícitos y filtros y pm en Gradle: ficheros, catálogo y dependencias; los dos están en la Chuleta de adb.
Con la app cerrada. Ciérrala del todo (en el emulador, desliza hacia arriba desde la barra de gestos para ver las apps recientes y desliza la tarjeta de la Pokédex hacia arriba para descartarla) y lanza:
adb shell am start -a android.intent.action.VIEW -d 'pokedex://pokemon/25'Se abre la ficha de Pikachu. Pulsa volver: debe llevarte a la lista, no cerrar la app.
Con la app abierta. Con la Pokédex delante, lanza otro enlace, esta vez al 6:
adb shell am start -a android.intent.action.VIEW -d 'pokedex://pokemon/6'La pantalla cambia a Charizard. Es el caso de onNewIntent, y la orden responde Warning: Activity not started, intent has been delivered to currently running top-most instance.: es singleTop reutilizando la Activity. Pulsa volver: te lleva a la lista, y no a un Pikachu anterior. Después, con la ficha abierta, vuelve a la lista y gira el móvil (en el emulador, con los botones de rotar de su barra lateral; si no gira, activa la rotación automática en los ajustes rápidos): te quedas en la lista, gracias al if (savedInstanceState == null).
Prueba también pokedex://pokemon/pikachu con la misma orden: la app pasa al frente y no cambia de pantalla, porque parse devuelve null. Un enlace mal escrito no rompe nada, y eso es lo que fija el test loQueNoEntendemosNoRevienta, el que escribiste en El mapa de pantallas compartido.
Con la verificación. Estas dos órdenes abren el enlace https y preguntan por el estado de la verificación:
adb shell am start -a android.intent.action.VIEW -d 'https://sebastiannarvaez11.github.io/pokedex-kmp/pokemon/25'
adb shell pm get-app-links com.sebastiannarvaez.pokedexEn estas órdenes (y en la de más abajo), com.sebastiannarvaez.pokedex es el identificador de la app: si en Crear el proyecto Pokédex pusiste otro applicationId, usa el tuyo.
La primera abre el navegador, no la app, y cae en la página del curso que invita a abrir la app. Si es la primera vez que abres Chrome en el emulador, enseña antes una pantalla de bienvenida: acéptala o pulsa «No thanks». La segunda enseña la huella de firma de tu app (Signatures, la que iría en assetlinks.json) y el estado de la verificación del dominio:
Signatures: [90:1F:FE:…:D6:84]
Domain verification state:
sebastiannarvaez11.github.io: 1024Tu huella será distinta de esta: es la de tu certificado de depuración. El estado puede ser un número de 1024 o más (un código de error del verificador del dispositivo; en Android 15 sale 1024) o una palabra, como legacy_failure, según la versión. Lo importante es que sea cualquier cosa que no sea verified.
Aprobar el dominio a mano (opcional)
Este apartado es opcional: sirve para ver el https abriendo la app, pero no hace falta para seguir el curso. Lo que la verificación hace sola, el usuario puede hacerlo a mano: permitir que la app abra los enlaces de ese dominio, en los ajustes de la app. Desde la Terminal se hace con pm set-app-links-user-selection (--user cur significa «para el usuario actual del emulador»), y después se vuelve a lanzar el enlace https:
adb shell pm set-app-links-user-selection --user cur --package com.sebastiannarvaez.pokedex true sebastiannarvaez11.github.io
adb shell am start -a android.intent.action.VIEW -d 'https://sebastiannarvaez11.github.io/pokedex-kmp/pokemon/25'Ahora el https abre la ficha de Pikachu en tu app, sin haber verificado nada. La verificación existe para que no haga falta pedírselo a nadie. Se deshace con la misma orden cambiando true por false.
Punto de control
./gradlew :shared:jvmTesttermina enBUILD SUCCESSFUL, con los cinco tests deDestinationTesty ninguno en rojo. Esta lección no añade tests: siguen los 76 de la JVM.- Con la app cerrada, el
adb shell am startdel esquema propio abre la ficha de Pikachu. - Con la app abierta, el mismo comando con otro número cambia la pantalla a ese Pokémon.
- Desde la ficha abierta por enlace, volver lleva a la lista y no cierra la app.
adb shell pm get-app-linksresponde, para el dominio, un estado distinto deverified(por ejemplo1024).- El enlace
httpsabre el navegador en la página del curso que invita a abrir la app. - (Opcional) Tras aprobar el dominio a mano, el mismo enlace
httpsabre la ficha de Pikachu en la app.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.Lanzas un enlace con la app ya abierta: la pantalla no cambia y, al volver atrás, reaparece la de antes. ¿Qué falta?
2.
adb shell pm get-app-linksno muestraverifiedpara el dominio, sino un número como1024(o una palabra comolegacy_failure). ¿Qué significa?3.¿Por qué el esquema propio
pokedex://es el camino principal y no el enlacehttps?4.Al abrir una ficha por enlace, la pila se vacía y se apilan la lista y el detalle. ¿Por qué no solo el detalle?
5.¿Por qué
Destination.parsedevuelvenullen vez de lanzar una excepción?6.Abres la app con un enlace a Pikachu, vuelves a la lista y giras el móvil. ¿Por qué no reaparece la ficha?