Principes de base des applications

Créer des liens profonds avec sûreté du typage avec Jetpack Navigation3

Haut-parleurs

  • Clara Fok

    Ingénieur logiciel

  • Ian Lake

    Responsable ingénierie

À propos

À propos de l'intervention

La gestion des liens profonds dans les applications Android modulaires peut facilement entraîner des routes défectueuses et des surprises lors de l'exécution.Cette session présente la nouvelle architecture de liens profonds de Jetpack Navigation3 pour Compose, qui permet de créer un système de routage robuste et évolutif. Nous verrons comment implémenter des API de mise en correspondance des liens profonds prêtes à l'emploi, écrire des sérialiseurs personnalisés pour les arguments complexes avec sûreté du typage et créer des piles d'activité hiérarchiques dynamiques qui permettent de conserver une navigation prévisible pour l'utilisateur.

Résumé

  • Les API de liens profonds Navigation 3 convertissent les requêtes de liens profonds entrantes en clés de navigation pour déterminer votre pile "Retour" initiale. La classe DeepLinkRequest compatible avec KMP encapsule l'entrée, y compris un constructeur d'intent Android.
  • Un UriDeepLinkMatcher utilise la sérialisation Kotlin pour faire correspondre les modèles d'URI et désérialiser les arguments en propriétés NavKey avec sûreté du typage. Les types primitifs fonctionnent automatiquement, et les correspondances échouent si les valeurs ne peuvent pas être analysées.
  • Les développeurs peuvent étendre DeepLinkSerializer ou remplacer matchArguments pour gérer les chaînes de liens profonds complexes. La sous-classe UriDeepLinkMatcher vous permet de prétraiter les espaces réservés d'URI bruts avant l'exécution de la sérialisation Kotlin.
  • L'appel de withBackStack sur les outils de mise en correspondance crée des piles "Retour" synthétiques de manière différée pour la meilleure correspondance. Cela garantit un comportement de retour du système approprié sans calculer de piles "Retour" inutiles pour chaque autre correspondance valide.

Ressources

Gemini Notebook

Thèmes

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

Transcription

Introduction et recettes Navigation 3

  • Bonjour, je m'appelle Ian Lake et je suis responsable de l'ingénierie pour l'équipe Jetpack Architecture.

  • Et je m'appelle Clara Fok, responsable technique pour Nav 3.

  • Aujourd'hui, nous allons vous parler d'une fonctionnalité qui me tient particulièrement à cœur : les liens profonds.

  • En fait, je les aime tellement que j'ai implémenté les deux pour Nav2 et Nav3 dans le cadre de la version 1.2 de Nav3.

  • Je pense que vous l'avez fait trois fois, car nous avons également effectué une implémentation dans les recettes Nav 3.

  • Oui, c'est vrai.

  • Nous avons procédé à de nombreuses itérations sur l'API pour nous assurer qu'elle s'intègre parfaitement à l'approche Compose-first de Nav3.

  • Et cela a commencé en grande partie par l'accent que nous avons mis sur les recettes.

  • Oui, l'un de nos principaux objectifs pour Navigation 3 était de ne pas vous gêner.

  • Lorsque vous souhaitez créer une fonctionnalité particulièrement ambitieuse, vous ne devriez pas avoir à nous envoyer une demande de fonctionnalité pour pouvoir la réaliser.

  • Alors, quand on nous demande "Comment faire ?",

  • ou "Comment aborder ce problème ?"

  • Au lieu de créer une API personnalisée dès le départ, nous créons une recette, un guide qui explique comment ajouter cette fonctionnalité à votre application sans nécessairement avoir à modifier Navigation 3.

  • C'est cette approche, qui consiste à montrer plutôt qu'à expliquer, qui nous a amenés à créer le dépôt de recettes Nav3 sur GitHub.

  • Il contient désormais plus de 35 recettes pour des cas d'utilisation courants, comme les piles de retour multiples et les bottom sheets, qui vous fournissent tout le code nécessaire pour les implémenter dans votre application.

  • Cela signifie que même pour la version 1.0 de Nav3, en novembre 2025, nous avions déjà une recette pour implémenter les liens profonds, car nous savions que cela était important pour beaucoup d'entre vous.

  • Mais lorsque nous nous rendons compte qu'une recette nécessite d'avoir un ensemble de méthodes d'assistance que vous devez copier et coller dans votre application, nous envisageons un processus appelé "graduation", où nous prenons ces assistants et nous nous demandons : "À quoi cela ressemblerait-il s'ils faisaient partie de Navigation 3 ?"

  • Cela garantit que les développeurs qui ont déjà copié-collé ce code ne rencontrent pas de problème.

  • Vous pouvez conserver ce code.

  • Si vous avez des solutions personnalisées qui fonctionnent, c'est formidable.

  • Vous n'avez aucune migration forcée.

  • Mais s'il s'agit d'un tout nouveau problème ou d'une nouvelle exigence, vous obtenez une solution simple et validée que vous pouvez utiliser directement dans le cadre de la navigation.

  • Mais le plus intéressant, pour moi personnellement, c'est qu'après avoir ajouté cet assistant, nous revenons en arrière et mettons à jour nos recettes, et nous obtenons cette agréable sensation de dopamine en supprimant du code.

  • Ainsi, toute personne qui consulte les recettes pour trouver l'approche recommandée pour ce problème dispose de la solution la plus récente.

  • Bien sûr, avec le recul, ce processus de graduation nous donne l'occasion d'examiner de plus près les assistants que nous avons créés dans les recettes et de nous demander s'il s'agit vraiment des meilleures API pour cela.

  • Dans ce cas, cela nous donne l'occasion d'améliorer ces éléments lorsque nous les déplaçons dans la bibliothèque.

  • Et c'était sans aucun doute le cas pour les liens profonds.

Conception d'API de liens profonds et mise en correspondance de formats d'URI

  • Merci, Ian.

  • Conceptuellement, les recettes et nos API finales sont donc très similaires, car dans Nav 3, vous possédez la pile "Retour".

  • Cela signifie que nos API de liens profonds se concentrent sur la détermination de cette pile "Retour" initiale, en particulier sur l'écran que vos utilisateurs voient lorsqu'ils cliquent sur un lien profond pour accéder à votre application.

  • Mais que se passe-t-il réellement lorsque l'utilisateur clique sur cet URI Web ?

  • Désormais, que vous utilisiez Nav3 ou non, le framework Android vous oblige à déclarer des filtres d'intent pour chaque lien profond compatible avec votre application.

  • Ce qui est intéressant, c'est ce qui se passe une fois que ce lien profond déclenche le démarrage de votre activité.

  • Ici, l'entrée est l'intent avec lequel votre application est lancée, l'activité qui a démarré.

  • Mais le lien profond peut prendre de nombreuses formes.

  • Par exemple, vous pouvez la créer manuellement pour un test ou profiter de la compatibilité avec Kotlin Multiplatform.

  • Nous avons donc encapsulé l'entrée de lien profond dans une classe DeepLinkRequest compatible avec KMP.

  • Sur Android, nous avons un constructeur DeepLinkRequest qui accepte un intent Android.

  • Ensuite, notre objectif est de déterminer, pour une requête donnée, comment savoir quel NavKey doit être placé dans votre pile "Retour".

  • C'est là que notre classe DeepLinkMatcher entre en jeu.

  • Elle accepte une requête et génère un MatchResult, qui contient le NavKey associé à la correspondance.

  • Pour les URI Web, nous disposons d'un UriDeepLinkMatcher qui encapsule toute la logique nécessaire pour transformer une requête donnée en MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Le premier paramètre d'un UriDeepLinkMatcher est désormais un format d'URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Imaginons que vous développiez une application ADS et que vous souhaitiez ajouter un lien profond vers l'écran d'accueil.

  • Ici, le modèle est un URI exact, et nous pouvons omettre le schéma HTTP pour qu'il corresponde automatiquement à HTTP et HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Mais si nous voulions créer un lien profond vers une session publicitaire ADS particulière, ce lien profond contiendrait désormais des informations que nous devons extraire, à savoir l'ID de session.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • Dans ce cas, notre modèle utilise des accolades comme espaces réservés pour les informations que nous devons extraire du lien profond.

  • Il s'avère qu'il existe déjà une bibliothèque pratique pour convertir n'importe quel type d'entrée en instances de classe : Kotlin Serialization.

  • Notre UriDeepLinkMatcher utilise donc un décodeur Kotlin pour désérialiser un URI d'entrée et générer une instance NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Ce décodeur doit savoir avec quelle classe travailler. Vous devez donc également transmettre un sérialiseur pour votre NavKey au comparateur.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Pour aider le décodeur à faire correspondre correctement les espaces réservés aux propriétés de la clé, le nom de l'espace réservé doit correspondre aux noms des propriétés.

  • Dans cet exemple, le nom de la propriété est "ID", qui correspond au paramètre d'ID de la session.

  • Maintenant que nous avons notre UriDeepLinkMatcher, nous devons créer un DeepLinkRequest avec le lien profond entrant.

    // 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)
  • La mise en correspondance est ensuite effectuée en appelant la fonction de correspondance et en transmettant notre requête.

  • Vous obtiendrez ainsi un MatchResult nullable contenant le NavKey, qui, dans notre exemple, serait la clé de session. Si le résultat est nul, nous pouvons simplement utiliser l'écran d'accueil par défaut.

  • Désormais, plusieurs liens profonds doivent être écrits sous la forme de plusieurs outils de mise en correspondance : un pour chaque lien profond.

  • Par exemple, nous avons ici deux formats d'URI pour notre clé de session : un pour un ancien format de newsletter et un pour un URI Web standard.

  • Nous avons également un lien profond vers l'écran d'accueil ADS. Nous ajoutons donc un troisième outil de mise en correspondance pour cela.

  • Tous les comparateurs seraient ajoutés à la même liste que nous parcourons.

  • Notre correspondance devient donc une boucle sur tous les outils de mise en correspondance de notre liste, où nous appelons la correspondance sur chacun d'eux pour trouver tous les outils de mise en correspondance qui correspondent à la requête.

  • Pour ce faire, nous pouvons utiliser mapNotNull pour filtrer les non-correspondances et obtenir une liste des correspondances valides uniquement.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • Dans la plupart des cas, vous n'aurez probablement qu'une seule correspondance non nulle dans votre liste, mais il peut arriver qu'il y ait plusieurs correspondances valides.

  • Par exemple, supposons que nous introduisions un écran de keynote spécial réutilisant le même modèle d'URI de session, mais qu'au lieu de l'espace réservé à l'ID, il comporte désormais un chemin d'accès statique à la keynote.

  • Dans ce cas, la même requête peut correspondre aux deux outils de mise en correspondance : la clé de session où l'ID est égal à "keynote" et la NavKey "keynote".

  • Comme MatchResult implémente Comparable, nous pouvons trouver la meilleure correspondance en comparant toutes les correspondances valides de la liste à l'aide de la fonction max, ou plutôt maxOrNull pour gérer le cas où aucun lien profond ne correspond à la requête.

    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
    )
  • Une liste statique de comparateurs peut être utile pour un Lightning Talk, mais tout comme vos écrans peuvent être répartis sur plusieurs modules, nous nous attendons à ce que ces mêmes modules contribuent à une liste globale de comparateurs via votre système d'injection de dépendances.

    // 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>>
  • En utilisant cette approche, nous avons mis à jour nos recettes Hilt, Metro et Koin dans notre dépôt de recettes pour inclure des liens profonds.

Désérialisation avec sûreté de typage et sérialiseurs personnalisés

  • Clara a mentionné que notre UriDeepLinkMatcher utilise la sérialisation Kotlin pour extraire les champs de la requête de lien profond et les intégrer à notre classe avec sûreté du typage, comme la classe de session que nous avons présentée.

  • Comme indiqué ici, il s'agit d'une transformation assez simple.

  • L'URI est une chaîne.

  • Le champ est une chaîne.

  • C'était vraiment facile.

  • Mais nous savions que nous devions aller au-delà des simples propriétés de chaîne.

  • C'est là que la sérialisation Kotlin nous fournit déjà une base solide sur laquelle travailler.

  • Tous les types primitifs, tels que les booléens, les entiers, les floats et même les énumérations, fonctionnent d'emblée. La correspondance échoue automatiquement et renvoie la valeur "null" si la valeur du lien profond ne peut pas être analysée comme ce type spécifique.

  • Par exemple, si le paramètre d'ID de notre session était un entier au lieu d'une chaîne, la transmission d'une chaîne qui ne pourrait pas être analysée en tant qu'entier entraînerait l'échec de la correspondance.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • De même, si vous souhaitez avoir des arguments facultatifs, par exemple pour les données d'analyse associées à votre URL Web, il peut s'agir de propriétés de votre clé ayant des valeurs par défaut.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Vous remarquerez que nous avons fait de notre mieux pour dissocier la structure de votre URL Web (qui peut appartenir à une autre équipe) des classes de sûreté du typage ou des classes qui représentent la correspondance de lien profond dans votre application.

  • Je sais que certains d'entre vous ont affaire à des liens profonds absolument 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
  • Par exemple, vous pouvez avoir une image encodée en base64 associée à votre lien profond.

  • Par exemple, un blob JSON géant qu'ils ont décidé d'ajouter à chaque URL de votre application.

  • Il existe une solution, même si vous ne pouvez pas modifier l'URL Web qui vous est fournie, mais que vous devez tout de même en extraire des informations.

  • La première technique consiste à étendre notre classe DeepLinkSerializer pour contrôler exactement comment cet argument est désérialisé ou transformé d'une chaîne en propriété de type sécurisé.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer est un wrapper d'assistance autour de la sérialisation Kotlin qui simplifie l'implémentation de sa classe de sérialiseur, car dans ce cas, l'entrée du désérialiseur est toujours une chaîne de votre URL Web.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Pour implémenter un DeepLinkSerializer, vous devez écrire trois méthodes.

  • Tout d'abord, vous choisissez un nom de série.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Il n'est pas nécessaire d'utiliser un espace de noms, mais il doit être suffisamment unique pour que la sérialisation Kotlin sache quand utiliser votre sérialiseur.

  • Ensuite, vous devez implémenter la désérialisation, qui reçoit cette chaîne à partir de votre URL Web et qui est chargée de construire votre objet avec sûreté du typage.

    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)
      }
    
       ...
    
    }
  • Enfin, vous implémenterez la sérialisation, qui est l'approche inverse. Elle est utilisée si votre même classe de clé est utilisée dans des cas comme 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)
      }
    }
  • Vous allez maintenant appliquer votre sérialiseur personnalisé à la propriété qui possède cette classe un peu spéciale, et le UriDeepLinkMatcher Web se chargera du reste.

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

Prétraiter les arguments d'URL avec des outils de mise en correspondance personnalisés

  • Mais que se passe-t-il si vous ne voulez qu'une petite partie des données de cette chaîne base64 ou de ce JSON associé à votre URL Web ?

  • Dans ce cas, il peut être judicieux de prétraiter une partie de votre URL Web avant qu'elle ne soit transmise à Kotlin Serialization.

  • Pour gérer ces cas, vous pouvez étendre UriDeepLinkMatcher et remplacer sa méthode matchArguments.

  • Maintenant, peut-être que pour notre blob JSON, tout ce que nous voulons vraiment, c'est cet ID de session, et que le reste ne nous intéresse pas.

  • Nous créons donc notre sous-classe et remplaçons 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>? {
        ...
    }
  • Les entrées sont ces chaînes brutes, tous les espaces réservés qui ont été ajoutés à partir de votre chemin d'URI.

  • Cela nous permet de prétraiter et de modifier ces valeurs avant d'appeler 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)
    }
  • Nous pouvons maintenant avoir des valeurs simplifiées ici, et nous n'avons pas du tout besoin de nous soucier de la sérialisation Kotlin.

Créer des piles "Retour" synthétiques pour les liens profonds

  • Merci, Ian.

  • Nous espérons que vous n'aurez pas à utiliser souvent ces techniques, mais nous avons beaucoup réfléchi à la façon dont vous pouvez contrôler le comportement de mise en correspondance.

  • OK.

  • Nous savons maintenant comment obtenir un NavKey avec les outils de mise en correspondance des liens profonds, mais une pile "Retour" ne contenant que la clé de navigation n'offre pas forcément la meilleure expérience utilisateur.

  • Pour garantir un comportement de retour approprié, vous devez donc utiliser une pile "Retour" synthétique.

  • La partie synthétique provient du fait que la pile "Retour" contient des clés vers lesquelles l'utilisateur n'a jamais navigué manuellement, mais il s'agit du chemin qu'il aurait emprunté s'il l'avait fait.

  • Par exemple, si l'utilisateur clique sur une notification pour une session qui est sur le point de commencer, vous l'emmenez directement à cette session. Toutefois, lorsqu'il déclenche le retour système, vous souhaitez probablement qu'il revienne à l'écran d'accueil avant de quitter le lanceur d'applications.

  • Cet écran d'accueil fait partie de la pile "Retour" synthétique.

  • Pour les API de liens profonds, nous voulions trouver un équilibre entre deux exigences contradictoires.

  • D'un côté, nous savons que différents liens profonds ou différents outils de mise en correspondance nécessitent une logique différente pour créer leur pile "Retour" synthétique.

  • Par exemple, l'ajout d'un HomeScreen avant chaque lien profond peut fonctionner jusqu'à ce que vous souhaitiez créer un lien profond vers un sous-écran dans un écran de paramètres.

  • Vous avez maintenant besoin d'une couche supplémentaire dans votre pile "Retour" synthétique pour ce lien profond particulier.

  • Naturellement, cela associe le compilateur de pile d'activité à chaque outil de mise en correspondance de liens profonds.

  • Mais d'un autre côté, nous savons aussi que chaque milliseconde qui s'écoule avant l'affichage de l'écran approprié vous coûte de l'argent.

  • Nous ne voulions donc pas calculer une pile "Retour" synthétique pour chaque résultat de match.

  • Nous voulions simplement la meilleure correspondance.

  • Alors, si nous voulons une logique personnalisée pour chaque outil de mise en correspondance, mais que nous ne voulons pas créer de pile d'historique synthétique de manière hâtive pour chaque correspondance, que devons-nous faire ?

  • Après quelques faux départs, nous avons trouvé une approche qui répond à ces deux exigences.

  • Quel que soit le type de l'outil de mise en correspondance, vous pouvez l'enrichir en appelant un withBackStack pour construire de manière différée la pile "Retour" avec le MatchResult donné.

    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)
  • Ainsi, si nous avons plusieurs outils de mise en correspondance, chacun d'eux aura son propre appel withBackStack pour créer la pile "Retour" appropriée pour ce NavKey spécifique.

  • Vous obtiendrez le code le plus propre si chaque outil de mise en correspondance que vous appelez utilise withBackStack, ce qui vous donnera une liste d'outils de mise en correspondance de la pile d'activité.

  • Lorsque vous trouvez la meilleure correspondance, le résultat renvoyé est un BackStackMatchResult.

  • Il s'agit d'une sous-classe de MatchResult qui ajoute une propriété appelée backStack, qui correspond à l'intégralité de la pile "Retour" générée par votre lambda withBackStack.

  • Cette approche par couches nous a permis de dissocier la logique des outils de mise en correspondance de la logique personnalisée que vous souhaitez utiliser dans votre lambda withBackStack.

Tour de table, résumé et ressources

  • Comme il s'agit d'un Lightning Talk, nous n'avons malheureusement plus beaucoup de temps, mais nous pouvons peut-être passer en revue quelques-unes des autres fonctionnalités de Navigation 3 que vous trouverez dans nos recettes Navigation 3.

  • OK.

  • Vous pouvez donc ajouter des filtres à n'importe quel outil de mise en correspondance pour court-circuiter la logique de mise en correspondance et exclure rapidement l'outil de mise en correspondance en fonction de l'action, du type MIME ou de vos propres filtres personnalisés.

  • Cela se produit avant l'analyse de l'URI ou la sérialisation personnalisée (sérialisation Kotlin).

  • Nous proposons également un StaticKeyDeepLinkMatcher, spécifiquement pour les cas où vous n'avez pas besoin d'effectuer d'analyse pour obtenir votre NavKey, mais où vous souhaitez tout de même appliquer des filtres.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Cette option est idéale pour gérer les liens profonds de type MIME ou d'action uniquement, comme une application de retouche photo capable d'ouvrir des images JPEG.

  • En parlant d'action et de type MIME, vous pouvez associer n'importe quel RequestExtras personnalisé à votre DeepLinkRequest, que vos filtres personnalisés et vos sous-classes DeepLinkRequest peuvent utiliser dans leur logique.

  • Cela vous permet d'intégrer des frameworks de tests A/B ou des signaux externes.

  • Nous disposons également d'API de sûreté du typage pour les extras.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Oh, et presque toute cette logique se trouve dans le code KMP commun de Nav3. Ainsi, tous vos outils de mise en correspondance et votre logique de pile d'activité peuvent être partagés entre les plates-formes.

  • Si vous souhaitez examiner plus en détail la façon dont Navigation 3 prend en charge les liens profonds, consultez nos recettes Nav3.

  • Oui, et nous avons également intégré nos API de liens profonds dans nos recettes d'injection de dépendances, ainsi que dans notre application phare, NowInAndroid.

  • Merci d'avoir assisté à notre Lightning Talk, et bonne navigation !

  • Merci.

Code

Extraits de code

Extrait de code Kotlin à 04:41 dans "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Extrait de code Kotlin à 04:53 dans "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Extrait de code Kotlin à 05:04 dans "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Extrait de code Kotlin à 05:12 dans "Conception d'API de liens profonds et correspondance de modèles d'URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Extrait de code Kotlin à 05:39 dans "Conception d'API de lien profond et correspondance de modèle d'URI"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Extrait Kotlin à 05:48 dans "Deep link API design and URI pattern matching"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Extrait de code Kotlin à 06:12 dans "Conception d'API de liens profonds et correspondance de modèles d'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)
Extrait de code Kotlin à 07:11 dans "Deep link API design and URI pattern matching"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Extrait Kotlin à 07:48 dans "Deep link API design and URI pattern matching"
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
)
Extrait de code Kotlin à 08:04 dans "Conception d'API de lien profond et correspondance de modèles d'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>>
Extrait de code Kotlin à 09:15 dans "Type-safe deserialization and custom serializers"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Extrait de code Kotlin à 09:27 dans "Désérialisation de type sécurisé et sérialiseurs personnalisés"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Extrait de code Kotlin à 09:53 dans "Désérialisation avec sûreté du typage et sérialiseurs personnalisés"
// "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
Extrait de code Kotlin à 10:19 dans "Désérialisation de type sécurisé et sérialiseurs personnalisés"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Extrait de code Kotlin à 10:32 dans "Désérialisation avec sûreté du typage et sérialiseurs personnalisés"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Extrait de code Kotlin à 10:51 dans "Désérialisation de type sécurisé et sérialiseurs personnalisés"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Extrait de code Kotlin à 11:01 dans "Désérialisation avec sûreté du typage et sérialiseurs personnalisés"
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)
  }

   ...

}
Extrait de code Kotlin à 11:13 dans "Désérialisation avec sûreté du typage et sérialiseurs personnalisés"
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)
  }
}
Extrait de code Kotlin à 11:24 dans "Désérialisation avec sûreté du typage et sérialiseurs personnalisés"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Extrait de code Kotlin à 12:09 dans "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>? {
    ...
}
Extrait Kotlin à 12:21 dans "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>? {
    val finalQueryArgs = buildMap {
      putAll(queryArgs)
      val jsonBlob = remove("session")?.firstOrNull()
      if (jsonBlob != null) { put("session", extractSessionIdFromJson(jsonBlob)) }
    }
    return super.matchArguments(pathArgs, finalQueryArgs, fragmentArgs)
}
Extrait de code Kotlin à 14:31 dans "Créer des piles de retour synthétiques pour les liens profonds"
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)
Extrait de code Kotlin à 15:52 dans "Lightning round, summary, and resources"
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Extrait de code Kotlin à 16:24 dans "Lightning round, summary, and resources"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}