Apps foundation

Cómo compilar vínculos directos con seguridad de tipos con Navigation3 de Jetpack

Bocinas

  • Clara Fok

    Ingeniero de software

  • Ian Lake

    Gerente de ingeniería

Acerca de

Acerca de la charla

Administrar vínculos directos en apps para Android modulares puede generar fácilmente rutas rotas y sorpresas en el tiempo de ejecución.En esta sesión, se analiza la nueva arquitectura de vinculación directa con seguridad de tipos de Jetpack Navigation3 para Compose, que te ayudará a crear un sistema de enrutamiento sólido y escalable. Exploraremos cómo implementar APIs de coincidencia de vínculos directos listas para usar, escribir serializadores personalizados para argumentos complejos con seguridad de tipos y construir pilas de actividades jerárquicas dinámicas que mantengan predecible la navegación del usuario.

Resumen

  • Las APIs de vínculos directos de Navigation 3 convierten las solicitudes de vínculos directos entrantes en claves de navegación para determinar tu pila de actividades inicial. La clase DeepLinkRequest compatible con KMP encapsula la entrada, incluido un constructor de intents de Android.
  • Un UriDeepLinkMatcher usa la serialización de Kotlin para hacer coincidir patrones de URI y deserializar argumentos en propiedades NavKey con seguridad de tipos. Los tipos primitivos funcionan automáticamente y fallan las coincidencias si no se pueden analizar los valores.
  • Los desarrolladores pueden extender DeepLinkSerializer o anular matchArguments para controlar cadenas de vínculos directos complejas. La creación de subclases de UriDeepLinkMatcher te permite preprocesar marcadores de posición de URI sin procesar antes de que se ejecute la serialización de Kotlin.
  • Llamar a withBackStack en los comparadores crea pilas de actividades sintéticas de forma diferida para obtener la mejor coincidencia. Esto garantiza un comportamiento correcto del sistema sin calcular pilas de actividades innecesarias para cada una de las demás coincidencias válidas.

Recursos

Gemini Notebook

Temas

  • jetpack-navigation-3
  • deep-links
  • kotlin-serialization
  • kotlin-multiplatform
  • android-architecture

Transcripción

Descargar transcripción

Introducción y recetas de Navigation 3

  • Hola, soy Ian Lake, gerente de ingeniería del equipo de arquitectura de Jetpack.

  • Y yo soy Clara Fok, la líder técnica de Nav 3.

  • Hoy hablaremos sobre una función que me encanta: los vínculos directos de Navigation.

  • De hecho, me gustan tanto que implementé Nav2 y Nav3 como parte de la versión 1.2 de Nav3.

  • Creo que lo hiciste tres veces, porque también hicimos una implementación en las recetas de Nav 3.

  • Sí, es verdad.

  • Realizamos muchas iteraciones en la API para asegurarnos de que se sienta como en casa con el enfoque centrado en Compose de Nav3.

  • Y gran parte de eso comenzó con nuestro énfasis en las recetas.

  • Sí, uno de nuestros objetivos principales para Navigation 3 era no interponernos en tu camino.

  • Cuando quieras crear una función particularmente ambiciosa, no debería ser necesario que nos envíes una solicitud de función para que sea posible.

  • Entonces, cuando nos preguntan: "Muy bien, ¿cómo hago esto?",

  • O bien, "¿Cómo puedo abordar este problema?".

  • En lugar de crear una API personalizada desde el principio, creamos una receta, una guía que explica cómo puedes agregar esa función a tu app sin necesidad de realizar cambios en Navigation 3.

  • Ese estilo de mostrar, no de contar, nos llevó a crear el repositorio de recetas de Nav3 en GitHub.

  • Ahora tiene más de 35 recetas para casos de uso comunes, como varias pilas de actividades y hojas inferiores, que te brindan todo el código que necesitas para implementarlas en tu app.

  • Eso significó que, incluso para la versión 1.0 de Nav3, en noviembre de 2025, ya teníamos una receta para implementar vínculos directos porque sabíamos que eso era importante para muchos de ustedes.

  • Pero cuando nos damos cuenta de que una receta requiere tener un montón de métodos auxiliares que debes copiar y pegar en tu app, es cuando consideramos un proceso llamado graduación, en el que tomamos esos auxiliares y decimos: "Muy bien, ¿cómo se vería eso si formaran parte de Navigation 3?".

  • Esto garantiza que los desarrolladores que ya copiaron y pegaron ese código no tengan problemas.

  • Puedes conservar ese código.

  • Si tienes soluciones personalizadas y funcionan, es genial.

  • No tienes ninguna migración forzada.

  • Pero si es un problema nuevo para ti o un requisito nuevo, obtienes una solución sencilla y verificada que te permite usarla directamente como parte de la navegación.

  • Pero la mejor parte, para mí, es que después de agregar ese ayudante, volvemos y actualizamos nuestras recetas y obtenemos ese dulce golpe de dopamina de borrar código.

  • De esa manera, cualquier persona que busque las recetas para el enfoque recomendado para ese problema tendrá la solución más actualizada.

  • Por supuesto, es fácil hablar con el diario del lunes, y ese proceso de graduación nos da la oportunidad de analizar realmente esos ayudantes que creamos en las recetas y decir: "¿Son realmente las mejores APIs para eso?".

  • En este caso, nos da la oportunidad de mejorar esos aspectos a medida que los trasladamos a la biblioteca.

  • Y ese fue, sin dudas, el caso de los vínculos directos.

Diseño de la API de vínculos directos y coincidencia con el patrón de URI

  • Gracias, Ian.

  • Por lo tanto, conceptualmente, tanto las recetas como nuestras APIs finales son muy similares, porque en Nav 3, tú eres el propietario de la pila de actividades.

  • Eso significa que el enfoque de nuestras APIs de vínculos directos es determinar cuál debería ser esa pila de actividades inicial, específicamente qué pantalla ven tus usuarios cuando hacen clic en un vínculo directo para ingresar a tu app.

  • Pero ¿qué sucede realmente cuando el usuario hace clic en ese URI web?

  • Ahora, ya sea que uses Nav3 o no, el framework de Android requiere que declares filtros de intents para cada vínculo directo que admita tu app.

  • Ahora, lo interesante es lo que sucede una vez que ese vínculo directo activa tu actividad para que se inicie.

  • Aquí, la entrada es el Intent con el que se inicia tu app, esa Activity que se inició.

  • Sin embargo, el vínculo directo puede adoptar muchas formas.

  • Por ejemplo, es posible que quieras construirlo de forma manual para una prueba o que quieras aprovechar la compatibilidad con Kotlin Multiplatform.

  • Por lo tanto, encapsulamos la entrada del vínculo directo en una clase DeepLinkRequest compatible con KMP.

  • En Android, tenemos un constructor DeepLinkRequest que toma un intent de Android.

  • A continuación, nuestro objetivo es averiguar, dada una solicitud, cómo sabemos qué NavKey se debe colocar en tu pila de actividades.

  • Aquí es donde entra en juego nuestra clase DeepLinkMatcher.

  • Toma una solicitud y genera un MatchResult, que contiene el NavKey asociado con la coincidencia.

  • Específicamente para los URIs web, tenemos un UriDeepLinkMatcher que encapsula toda la lógica necesaria para transformar una solicitud determinada en un MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Ahora, el primer parámetro que toma un UriDeepLinkMatcher es un patrón de URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Supongamos que estás creando una app de ADS y quieres agregar un vínculo directo a la pantalla principal.

  • Aquí, el patrón es un URI exacto, y podemos omitir el esquema HTTP para que coincida automáticamente con HTTP y HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Pero si quisiéramos crear un vínculo directo a una sesión publicitaria de ADS en particular, ahora ese vínculo directo contiene información que debemos extraer, es decir, el ID de sesión.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • En este caso, nuestro patrón usa llaves para que actúen como marcadores de posición de la información que necesitamos extraer del vínculo directo.

  • Resulta que ya existe una biblioteca útil para convertir cualquier tipo de entrada en instancias de clase, que es Kotlin Serialization.

  • Por lo tanto, nuestro UriDeepLinkMatcher aprovecha un decodificador de Kotlin para deserializar un URI de entrada y generar una instancia de NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Ahora, ese decodificador necesita saber con qué clase trabajar, por lo que también deberás pasar un serializador para tu NavKey al comparador.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Para ayudar al decodificador a hacer coincidir correctamente los marcadores de posición con las propiedades de la clave, el nombre del marcador de posición debe coincidir con los nombres de las propiedades.

  • En este ejemplo, el nombre de la propiedad es ID, que coincide con el parámetro ID de la sesión.

  • Ahora que tenemos nuestro UriDeepLinkMatcher, debemos construir un DeepLinkRequest con el vínculo directo entrante.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
    
    val request = DeepLinkRequest(
      uri = DeepLinkUri("http://ads.android.com/session/abcd")
    )
    
    val matchResult: UriMatchResult<Session>? = matcher.match(request)
    
    val backStack = listOf(matchResult?.key ?: Home)
  • Luego, se realiza la coincidencia llamando a la función match y pasando nuestra solicitud.

  • Esto te dará un MatchResult que puede ser nulo y que contiene el NavKey, que en nuestro ejemplo sería la clave de sesión, o si el resultado es nulo, podemos usar la pantalla principal de forma predeterminada.

  • Ahora, se deben escribir varios vínculos directos como varios comparadores: uno para cada vínculo directo.

  • Por ejemplo, aquí tenemos dos patrones de URI para nuestra clave de sesión: uno para un patrón de boletín informativo heredado y otro para un URI web normal.

  • También tenemos un vínculo directo a la pantalla principal de ADS, por lo que agregamos un tercer comparador para eso.

  • Y todos los comparadores se agregarían a la misma lista que iteramos.

  • Por lo tanto, ahora nuestra coincidencia se convierte en un bucle sobre todos los comparadores de nuestra lista, en el que llamamos a match en cada uno para encontrar todos los comparadores que coinciden con la solicitud.

  • Para ello, podríamos usar mapNotNull para filtrar las no coincidencias y obtener una lista de solo las coincidencias válidas.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • En la mayoría de los casos, es probable que solo tengas una coincidencia no nula en tu lista, pero hay casos en los que puede haber más de una coincidencia válida.

  • Por ejemplo, supongamos que presentamos una pantalla de keynote especial que reutiliza el mismo patrón de URI de sesión, pero ahora, en lugar del marcador de posición de ID, tiene una ruta de acceso de keynote estática.

  • Por lo tanto, en este caso, la misma solicitud puede coincidir con ambos comparadores: la clave de sesión en la que el ID es igual a keynote y la keynote NavKey.

  • Como MatchResult implementa Comparable, podemos encontrar la mejor coincidencia comparando todas las coincidencias válidas de la lista con la función max o, en realidad, maxOrNull para controlar el caso en el que no haya vínculos directos que coincidan con la solicitud.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
    
    // compare matches
    val backStack = listOf(
      matchResults.maxOrNull()?.key ?: Home
    )
  • Ahora bien, una lista estática de comparadores puede tener sentido para una charla breve, pero, al igual que tus pantallas se pueden distribuir en varios módulos, esperamos que esos mismos módulos contribuyan a una lista global de comparadores a través de tu sistema de DI.

    // Feature A Module
    @ContributesTo(ActivityScope::class)
    @BindingContainer
    object FeatureAModule {
        @IntoSet
        @Provides
        fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
          UriDeepLinkMatcher(
            uriPattern = DeepLinkUri("ads.android.com/featureA"),
            serializer<FeatureBKey>(),
          )
    }
    
    // in MainActivity
    @Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
  • Con este enfoque, nuestras recetas de Hilt, Metro y Koin en nuestro repositorio de recetas se actualizaron para incluir vínculos directos.

Deserialización con seguridad de tipos y serializadores personalizados

  • Clara mencionó que nuestro UriDeepLinkMatcher usa la serialización de Kotlin para extraer campos de la solicitud de vínculo directo y convertirlos en nuestra clase de tipo seguro, como la clase de sesión que mostramos.

  • Como se indica aquí, esta es una transformación bastante simple.

  • El URI es una cadena.

  • El campo es una cadena.

  • ¡Eso fue fácil!

  • Sin embargo, sabemos que necesitábamos ir más allá de las propiedades de cadena.

  • Aquí es donde la serialización de Kotlin ya nos brinda una base sólida para trabajar.

  • Todos los tipos primitivos, como los booleanos, los números enteros, los números de punto flotante y hasta las enumeraciones, funcionan de inmediato, lo que hace que la coincidencia falle automáticamente y se devuelva un valor nulo si el valor del vínculo directo no se puede analizar como ese tipo específico.

  • Por ejemplo, si el parámetro de ID de nuestra sesión fuera un número entero en lugar de una cadena, pasar una cadena que no se pudiera analizar como un número entero haría que la coincidencia fallara.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Del mismo modo, si quisieras tener argumentos opcionales, por ejemplo, para los datos de estadísticas adjuntos a tu URL web, podrían ser propiedades de tu clave que tengan valores predeterminados.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Notarás que nos esforzamos mucho por desvincular la estructura de tu URL web, que podría pertenecer a un equipo diferente, de las clases con seguridad de tipos o las clases que representan la coincidencia de vínculo directo en tu app.

  • Porque sé que algunos de ustedes están lidiando con vínculos directos absolutamente terribles.

    // "id={id}&base64={placeholderImage}"
    @Serializable data class Session(
      val id: String,
      val placeholderImage: Bitmap? = null
    ) : NavKey
    
    // "jsonBlob={user}"
    @Serializable data class Profile(
      val user: User,
    ) : NavKey
  • Por ejemplo, tal vez tengas una imagen codificada en base64 adjunta a tu vínculo directo.

  • Por ejemplo, un blob JSON gigante que decidieron incluir en cada URL de tu app.

  • Ahora, hay una forma de avanzar, incluso cuando no puedes cambiar la URL web que se te proporciona, pero aún necesitas extraer cierta información de ella.

  • Por lo tanto, la primera técnica consiste en extender nuestra clase DeepLinkSerializer para controlar exactamente cómo se deserializa o transforma ese argumento de una cadena en tu propiedad con seguridad de tipos.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer es un wrapper auxiliar de la serialización de Kotlin que simplifica la implementación de su clase de serializador, ya que, en este caso, la entrada del deserializador siempre es una cadena de tu URL web.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Ahora, para implementar un DeepLinkSerializer, debes escribir tres métodos.

  • Primero, elige un nombre de serie.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • No es necesario que tenga un espacio de nombres, pero debe ser lo suficientemente único como para que la serialización de Kotlin sepa cuándo usar tu serializador.

  • A continuación, debes implementar deserialize, que recibe esa String de tu URL web y se encarga de construir tu objeto con seguridad de tipos.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      override fun deserialize(value: String): Bitmap {
        val imageBytes = Base64.decode(value, Base64.DEFAULT)
       // This better be a small image…
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
      }
    
       ...
    
    }
  • Por último, implementarás la serialización, que es el enfoque opuesto, y se usa si la misma clase de clave se usa en casos como rememberNavBackStack.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
    
      ...
    
      override fun serialize(value: Bitmap): String {
        val byteArrayOutputStream = ByteArrayOutputStream()
        // Compress the bitmap into the output stream (e.g., JPEG or PNG)
        value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
        val byteArray = byteArrayOutputStream.toByteArray()
        // Encode the byte array to a Base64 string
        return Base64.encodeToString(byteArray, Base64.DEFAULT)
      }
    }
  • Ahora, aplicarás tu serializador personalizado a la propiedad que tiene esa clase que es un poco especial, y UriDeepLinkMatcher web se encargará del resto.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
    
    @Serializable data class Session(
      val id: String,
      @Serializable(BitmapSerializer::class)
      val placeholderImage: Bitmap? = null
    ) : NavKey

Preprocesa los argumentos de URL con comparadores personalizados

  • Pero ¿qué sucede si solo quieres una pequeña parte de los datos de esa cadena en Base64 o ese JSON que se adjuntó a tu URL web?

  • En ese caso, podría ser conveniente procesar previamente parte de tu URL web antes de que llegue a la serialización de Kotlin.

  • Para controlar esos casos, puedes extender UriDeepLinkMatcher y anular su método matchArguments.

  • Ahora, tal vez para nuestro BLOB de JSON, todo lo que realmente queremos es solo ese ID de sesión, como si en realidad no nos importara nada del resto de eso.

  • Por lo tanto, creamos nuestra subclase y anulamos matchArguments.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        ...
    }
  • Las entradas son esas cadenas sin procesar, todos los marcadores de posición que se agregaron desde tu ruta de URI.

  • Esto nos permite procesar previamente y editar esos valores antes de llamar a super.matchArguments.

    class SessionUriDeepLinkMatcher(
      uriPattern: DeepLinkUri
    ) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {
    
      override fun matchArguments(
        pathArgs: Map<String, List<String>>,
        queryArgs: Map<String, List<String>>,
        fragmentArgs: Map<String, List<String>>
      ): UriMatchResult<Session>? {
        val finalQueryArgs = buildMap {
          putAll(queryArgs)
          val jsonBlob = remove("session")?.firstOrNull()
          if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
        }
        return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
    }
  • Ahora, podemos tener valores simplificados aquí y no necesitamos meternos con la serialización de Kotlin.

Cómo compilar pilas de actividades sintéticas para vínculos directos

  • Gracias, Ian.

  • Esperamos que no tengas que recurrir a esas técnicas con frecuencia, pero pensamos mucho en cómo puedes controlar el comportamiento de coincidencia.

  • Bien.

  • Ahora que sabemos cómo obtener un NavKey con los comparadores de vínculos directos, es posible que una pila de actividades con solo la clave de navegación no proporcione la mejor experiencia del usuario.

  • Por lo tanto, para garantizar el comportamiento correcto de retroceso, te recomendamos que uses una pila de actividades sintética.

  • La parte sintética proviene del hecho de que la pila de actividades contiene claves a las que el usuario nunca navegó de forma manual, pero es la ruta que habría tomado si lo hubiera hecho.

  • Por ejemplo, si el usuario hace clic en una notificación de una sesión que está por comenzar, lo llevarás directamente a esa sesión, pero, cuando active el botón Atrás del sistema, probablemente quieras que vuelva a la pantalla principal antes de salir al selector.

  • Esa HomeScreen se convierte en parte de la pila de actividades sintética.

  • En el caso de las APIs de vínculos directos, queríamos equilibrar dos requisitos contrapuestos.

  • Por un lado, sabemos que diferentes vínculos directos o diferentes comparadores querrían una lógica diferente para compilar su pila de actividades sintética.

  • Por ejemplo, agregar una HomeScreen antes de cada vínculo directo podría funcionar hasta que quieras vincular directamente a una pantalla secundaria dentro de una pantalla de configuración.

  • Ahora, necesitas una capa adicional en tu pila de actividades sintética para ese vínculo directo en particular.

  • Naturalmente, esto vincula el compilador de la pila de actividades con cada comparador de vínculos directos.

  • Pero, por otro lado, también sabemos que cada milisegundo que transcurre hasta que se muestra la pantalla correcta les cuesta dinero real.

  • Por lo tanto, no queríamos calcular una pila de actividades sintética para cada resultado de coincidencia.

  • Solo queríamos la mejor coincidencia.

  • Por lo tanto, si queremos una lógica personalizada para cada comparador, pero no queremos crear una pila de actividades sintética de forma inmediata para cada coincidencia, ¿qué hacemos?

  • Por lo tanto, después de algunos intentos fallidos, encontramos un enfoque que equilibra ambos requisitos.

  • Sin importar el tipo de comparador que tengas, puedes aumentarlo llamando a un withBackStack para construir de forma diferida la pila de actividades con el MatchResult determinado.

    val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, matchResult.key)
    }
    
    val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
      listOf(Home, SessionList, matchResult.key)
    }
    
    val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
      homeMatcher, sessionMatcher
    )
    
    val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
    val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()
    
    val backStack = bestMatch?.backStack ?: listOf(Home)
  • Por lo tanto, si tenemos varios comparadores, cada uno tendría su propia llamada a withBackStack para compilar la pila de actividades adecuada para ese NavKey específico.

  • Obtendrás el código más limpio si cada comparador que llames usa el withBackStack, que te dará una lista de comparadores de pila de actividades.

  • Y cuando encuentras la mejor coincidencia, el resultado que obtienes es un BackStackMatchResult.

  • Esta es una subclase de MatchResult que agrega una propiedad nueva llamada backStack, que es toda la pila de actividades que genera tu lambda withBackStack.

  • Este enfoque de capas nos permitió desvincular la lógica dentro de los comparadores de cualquier lógica personalizada que quieras usar dentro de tu expresión lambda withBackStack.

Ronda rápida, resumen y recursos

  • Como esta es una charla breve, lamentablemente, ya casi no nos queda tiempo, pero tal vez podamos revisar algunas de las otras funciones que tenemos en Navigation 3 y que encontrarás en nuestras recetas de Navigation 3.

  • Bien.

  • Por lo tanto, puedes agregar filtros a cualquier comparador para omitir la lógica de coincidencia y excluir rápidamente el comparador según la acción, el tipo de MIME o tus propios filtros personalizados.

  • Y esto sucede antes de pasar por cualquier análisis de URI o serialización personalizada, la serialización de Kotlin.

  • También ofrecemos un StaticKeyDeepLinkMatcher, específicamente para los casos en los que no necesitas realizar ningún análisis para obtener tu NavKey, pero de todos modos quieres aplicar filtros.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Esto es ideal para controlar vínculos directos solo de tipo MIME o de acción, como una app de edición de fotos que puede abrir imágenes JPEG.

  • Y hablando de acciones y tipos de MIME, puedes adjuntar cualquier RequestExtras personalizado a tu DeepLinkRequest, que tus filtros personalizados y subclases de DeepLinkRequest pueden usar como parte de su lógica.

  • Esto te permite combinar frameworks de pruebas A/B o indicadores externos.

  • También tenemos APIs completas con seguridad de tipos para los extras.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Ah, y casi toda esta lógica se encuentra en el código común de KMP de Nav3, por lo que todos tus comparadores y la lógica de la pila de actividades se pueden compartir entre plataformas.

  • Por lo tanto, si quieres analizar con más detenimiento cómo Navigation 3 admite vínculos directos, consulta nuestras recetas de Nav3.

  • Sí, y también integramos nuestras APIs de vínculos directos en nuestras recetas de DI, así como en nuestra app de ejemplo, NowInAndroid.

  • Gracias por asistir a nuestra charla breve. Esperamos que disfrutes la navegación.

  • Gracias.

Código

Fragmentos de código

Fragmento de Kotlin en el minuto 04:41 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Fragmento de Kotlin en el minuto 4:53 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Fragmento de Kotlin en el minuto 05:04 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Fragmento de Kotlin en el minuto 05:12 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Fragmento de Kotlin en el minuto 05:39 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Fragmento de Kotlin en el minuto 05:48 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Fragmento de Kotlin en el minuto 06:12 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)

val request = DeepLinkRequest(
  uri = DeepLinkUri("http://ads.android.com/session/abcd")
)

val matchResult: UriMatchResult<Session>? = matcher.match(request)

val backStack = listOf(matchResult?.key ?: Home)
Fragmento de Kotlin en el minuto 7:11 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Fragmento de Kotlin en el minuto 07:48 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}

// compare matches
val backStack = listOf(
  matchResults.maxOrNull()?.key ?: Home
)
Fragmento de Kotlin en el minuto 8:04 de "Diseño de API de vínculo directo y coincidencia de patrones de URI"
// Feature A Module
@ContributesTo(ActivityScope::class)
@BindingContainer
object FeatureAModule {
    @IntoSet
    @Provides
    fun provideDeepLinkMatcher() : UriDeepLinkMatcher<NavKey> =
      UriDeepLinkMatcher(
        uriPattern = DeepLinkUri("ads.android.com/featureA"),
        serializer<FeatureBKey>(),
      )
}

// in MainActivity
@Inject lateinit var matchers: Set<UriDeepLinkMatcher<NavKey>>
Fragmento de Kotlin en el minuto 09:15 de "Deserialización con seguridad de tipos y serializadores personalizados"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Fragmento de Kotlin en el minuto 09:27 de "Deserialización con seguridad de tipos y serializadores personalizados"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Fragmento de Kotlin en el minuto 9:53 de "Deserialización con seguridad de tipos y serializadores personalizados"
// "id={id}&base64={placeholderImage}"
@Serializable data class Session(
  val id: String,
  val placeholderImage: Bitmap? = null
) : NavKey

// "jsonBlob={user}"
@Serializable data class Profile(
  val user: User,
) : NavKey
Fragmento de Kotlin en el minuto 10:19 de "Deserialización con seguridad de tipos y serializadores personalizados"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Fragmento de Kotlin en el minuto 10:32 de "Deserialización con seguridad de tipos y serializadores personalizados"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Fragmento de Kotlin en el minuto 10:51 de "Deserialización con seguridad de tipos y serializadores personalizados"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Fragmento de Kotlin en el minuto 11:01 de "Deserialización con seguridad de tipos y serializadores personalizados"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  override fun deserialize(value: String): Bitmap {
    val imageBytes = Base64.decode(value, Base64.DEFAULT)
   // This better be a small image…
    return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
  }

   ...

}
Fragmento de Kotlin en el minuto 11:13 de "Deserialización con seguridad de tipos y serializadores personalizados"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {

  ...

  override fun serialize(value: Bitmap): String {
    val byteArrayOutputStream = ByteArrayOutputStream()
    // Compress the bitmap into the output stream (e.g., JPEG or PNG)
    value.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
    val byteArray = byteArrayOutputStream.toByteArray()
    // Encode the byte array to a Base64 string
    return Base64.encodeToString(byteArray, Base64.DEFAULT)
  }
}
Fragmento de Kotlin en el minuto 11:24 de "Deserialización con seguridad de tipos y serializadores personalizados"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Fragmento de Kotlin en el minuto 12:09 de "Preprocessing URL arguments with custom matchers"
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    ...
}
Fragmento de Kotlin en el minuto 12:21 de "Preprocesa argumentos de URL con comparadores personalizados"
class SessionUriDeepLinkMatcher(
  uriPattern: DeepLinkUri
) : UriDeepLinkMatcher(uriPattern, serializer=serializer<Session>()) {

  override fun matchArguments(
    pathArgs: Map<String, List<String>>,
    queryArgs: Map<String, List<String>>,
    fragmentArgs: Map<String, List<String>>
  ): UriMatchResult<Session>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
Fragmento de Kotlin en el minuto 14:31 de "Cómo compilar pilas de actividades sintéticas para vínculos directos"
val homeMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, matchResult.key)
}

val sessionMatcher = UriDeepLinkMatcher(...).withBackStack { matchResult ->
  listOf(Home, SessionList, matchResult.key)
}

val backStackMatchers: List<BackStackMatcher<NavKey, NavKey>> = listOf(
  homeMatcher, sessionMatcher
)

val matchResults = backStackMatchers.mapNotNull { matcher.match(request) }
val bestMatch: BackStackMatchResult<NavKey, NavKey>? = matchResults.maxOrNull()

val backStack = bestMatch?.backStack ?: listOf(Home)
Fragmento de Kotlin en el minuto 15:52 de "Lightning round, summary, and resources"
@Serializable object ImagesKey: NavKey

// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Fragmento de Kotlin en el minuto 16:24 de "Lightning round, summary, and resources"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}