Nozioni di base sulle app

Creazione di deep link con sicurezza dei tipi con Jetpack Navigation3

Altoparlanti

  • Clara Fok

    Software Engineer

  • Ian Lake

    Engineering Manager

Informazioni

Informazioni sulla presentazione

La gestione dei deep link nelle app per Android modulari può facilmente portare a percorsi interrotti e sorprese in fase di runtime.Questa sessione illustra la nuova architettura di deep linking con sicurezza dei tipi di Jetpack Navigation3 per Compose, per aiutarti a creare un sistema di routing solido e scalabile. Scopriremo come implementare API di corrispondenza dei deep link pronte all'uso, scrivere serializzatori personalizzati per argomenti complessi con sicurezza dei tipi e creare back stack gerarchici dinamici che mantengono prevedibile la navigazione dell'utente.

Riepilogo

  • Le API di deep link di Navigation 3 trasformano le richieste di deep link in entrata in chiavi di navigazione per determinare il back stack iniziale. La classe DeepLinkRequest compatibile con KMP incapsula l'input, incluso un costruttore di intent Android.
  • Un UriDeepLinkMatcher utilizza la serializzazione Kotlin per trovare i pattern URI corrispondenti e deserializzare gli argomenti in proprietà NavKey con sicurezza dei tipi. I tipi primitivi funzionano automaticamente, non riuscendo a trovare corrispondenze se i valori non possono essere analizzati.
  • Gli sviluppatori possono estendere DeepLinkSerializer o eseguire l'override di matchArguments per gestire stringhe di deep link complesse. La sottoclasse UriDeepLinkMatcher consente di pre-elaborare i segnaposto URI non elaborati prima dell'esecuzione della serializzazione Kotlin.
  • La chiamata di withBackStack sui matcher crea back stack sintetici in modo lazy per la corrispondenza migliore. In questo modo, il sistema si comporta correttamente quando l'utente torna indietro, senza calcolare back stack non necessari per ogni altra corrispondenza valida.

Risorse

Gemini Notebook

Argomenti

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

Trascrizione

Scarica la trascrizione

Introduzione e navigazione 3 ricette

  • Ciao, sono Ian Lake, Engineering Manager del team Jetpack Architecture.

  • Sono Clara Fok, Tech Lead di Nav 3.

  • Oggi siamo qui per parlarvi di una funzionalità che mi sta molto a cuore: i deep link.

  • In effetti, mi piacciono così tanto che ho eseguito l'implementazione sia per Nav2 che per Nav3 nell'ambito della release 1.2 di Nav3.

  • Penso che tu l'abbia fatto tre volte, perché abbiamo anche eseguito un'implementazione nelle ricette di Nav 3.

  • Sì, è vero.

  • Abbiamo eseguito molte iterazioni sull'API per assicurarci che sia perfettamente integrata con l'approccio incentrato su Compose di Nav3.

  • E gran parte di questo è iniziato con la nostra enfasi sulle ricette.

  • Sì, uno dei nostri obiettivi principali per Navigation 3 era non intralciare l'utente.

  • Quando devi creare una funzionalità particolarmente ambiziosa, non dovresti aver bisogno di una richiesta di funzionalità da parte nostra per renderla effettivamente possibile.

  • Quindi, quando ci viene posta la domanda "Ok, come faccio?",

  • O ancora: "Come posso affrontare questo problema?"

  • Anziché creare un'API personalizzata fin dall'inizio, creiamo una ricetta, una guida che spiega come aggiungere questa funzionalità alla tua app senza necessariamente dover apportare modifiche a Navigation 3.

  • È stato questo stile di mostrare, non di raccontare, che ci ha portato a creare il repository di formule Nav3 su GitHub.

  • Ora contiene oltre 35 ricette per casi d'uso comuni, come più back stack e riquadri inferiori, che ti forniscono tutto il codice necessario per l'implementazione nella tua app.

  • Ciò significa che anche per la versione 1.0 di Nav3, rilasciata a novembre 2025, avevamo già una soluzione per l'implementazione dei deep link, perché sapevamo che era importante per molti di voi.

  • Ma quando ci rendiamo conto che una ricetta richiede un sacco di metodi helper che devi copiare e incollare nella tua app, è allora che prendiamo in considerazione un processo chiamato "graduation", in cui prendiamo questi helper e diciamo: "Ok, come sarebbe se facessero parte di Navigation 3?".

  • In questo modo, gli sviluppatori che hanno già copiato e incollato il codice non avranno problemi.

  • Puoi conservare il codice.

  • Se hai soluzioni personalizzate che funzionano, è fantastico.

  • Non hai migrazioni forzate.

  • Ma se si tratta di un problema completamente nuovo per te o di un nuovo requisito, ottieni una soluzione semplice e verificata che ti consente di utilizzarla direttamente come parte della navigazione.

  • Ma la parte migliore, per me personalmente, è che dopo aver aggiunto l'helper, torniamo indietro e aggiorniamo le nostre ricette e otteniamo quella dolce scarica di dopamina che si prova quando si elimina il codice.

  • In questo modo, chiunque consulti le ricette per trovare l'approccio consigliato per un problema ha a disposizione la soluzione più aggiornata.

  • Naturalmente, con il senno di poi, questo processo di promozione ci dà l'opportunità di esaminare davvero gli helper che abbiamo creato nelle ricette e di chiederci: "Sono davvero le API migliori per questo scopo?"

  • In questo caso, ci dà l'opportunità di migliorare questi aspetti quando li spostiamo nella raccolta.

  • Questo è stato sicuramente il caso dei deep link.

Progettazione di API per i deep link e corrispondenza dei pattern URI

  • Grazie, Ian.

  • Quindi, concettualmente, sia le ricette che le nostre API finali sono molto simili, perché in Nav 3 sei tu a gestire il back stack.

  • Ciò significa che l'obiettivo delle nostre API di deep link è capire quale dovrebbe essere il back stack iniziale, in particolare quale schermata vedono gli utenti quando fanno clic su un deep link per accedere alla tua app.

  • Ma cosa succede effettivamente quando l'utente fa clic su quell'URI web?

  • Ora, indipendentemente dal fatto che utilizzi Nav3 o meno, il framework Android richiede di dichiarare i filtri per intent per ogni deep link supportato dalla tua app.

  • Ora, la parte interessante è ciò che accade quando il deep link attiva l'avvio dell'attività.

  • In questo caso, l'input è l'intent con cui viene avviata l'app, ovvero l'attività avviata.

  • Tuttavia, il deep link può assumere molte forme.

  • Ad esempio, potresti volerlo creare manualmente per un test o sfruttare il supporto di Kotlin Multiplatform.

  • Abbiamo quindi incapsulato l'input del deep link in una classe DeepLinkRequest compatibile con KMP.

  • Su Android, abbiamo un costruttore DeepLinkRequest che accetta un intent Android.

  • Ora, il nostro obiettivo è capire, data una richiesta, come facciamo a sapere cosa deve essere inserito nel back stack di NavKey?

  • È qui che entra in gioco la nostra classe DeepLinkMatcher.

  • Accetta una richiesta e restituisce un MatchResult, che contiene il NavKey associato alla corrispondenza.

  • In particolare, per gli URI web, abbiamo un UriDeepLinkMatcher che incapsula tutta la logica necessaria per trasformare una determinata richiesta in un MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Il primo parametro accettato da un UriDeepLinkMatcher è un pattern URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Supponiamo che tu stia creando un'app ADS e voglia aggiungere un deep link alla schermata Home.

  • In questo caso, il pattern è un URI esatto e possiamo omettere lo schema HTTP per trovare automaticamente corrispondenze sia con HTTP che con HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Tuttavia, se volessimo creare un deep link a una determinata sessione annuncio, questo deep link conterrebbe informazioni che dobbiamo estrarre, ovvero l'ID sessione.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • In questo caso, il nostro pattern utilizza le parentesi graffe come segnaposto per le informazioni che dobbiamo estrarre dal deep link.

  • E si scopre che esiste già una comoda libreria per convertire qualsiasi tipo di input in istanze di classe, ovvero Kotlin Serialization.

  • Quindi, il nostro UriDeepLinkMatcher utilizza un decoder Kotlin per deserializzare un URI di input e restituire un'istanza NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Ora, il decodificatore deve sapere con quale classe lavorare, quindi dovrai anche passare un serializzatore per NavKey nel matcher.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Per aiutare il decodificatore ad abbinare correttamente i segnaposto alle proprietà della chiave, il nome del segnaposto deve corrispondere ai nomi delle proprietà.

  • In questo esempio, il nome della proprietà è ID, che corrisponde al parametro ID della sessione.

  • Ok, ora che abbiamo il nostro UriDeepLinkMatcher, dobbiamo creare un DeepLinkRequest con il deep link in arrivo.

    // 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 corrispondenza viene quindi eseguita chiamando la funzione match e passando la nostra richiesta.

  • In questo modo otterrai un MatchResult nullable contenente NavKey, che nel nostro esempio sarebbe la chiave di sessione, oppure, se il risultato è null, possiamo semplicemente impostare la schermata Home come predefinita.

  • Ora, più deep link devono essere scritti come più matcher: un matcher per ogni deep link.

  • Ad esempio, qui abbiamo due pattern URI per la nostra chiave di sessione: uno per un pattern di newsletter legacy e uno per un URI web normale.

  • Abbiamo anche un deep link alla schermata Home di ADS, quindi aggiungiamo un terzo matcher.

  • Tutti i matcher verranno aggiunti allo stesso elenco su cui eseguiamo l'iterazione.

  • Ora la nostra corrispondenza diventa un loop su tutti i matcher nel nostro elenco, in cui chiamiamo la corrispondenza su ciascuno per trovare tutti i matcher che corrispondono alla richiesta.

  • Per farlo, possiamo utilizzare mapNotNull per filtrare le non corrispondenze e ottenere un elenco di sole corrispondenze valide.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • Nella maggior parte dei casi, probabilmente avrai una sola corrispondenza non null nell'elenco, ma in alcuni casi possono esserci più corrispondenze valide.

  • Ad esempio, supponiamo di introdurre una schermata speciale per il keynote riutilizzando lo stesso pattern URI della sessione, ma ora, anziché il segnaposto dell'ID, ha un percorso statico per il keynote.

  • In questo caso, la stessa richiesta può corrispondere a entrambi i matcher: la chiave di sessione in cui l'ID è uguale a keynote e keynote NavKey.

  • Poiché MatchResult implementa Comparable, possiamo trovare la corrispondenza migliore confrontando tutte le corrispondenze valide nell'elenco utilizzando la funzione max o maxOrNull per gestire il caso in cui non esistano deep link corrispondenti alla richiesta.

    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
    )
  • Ora, un elenco statico di matcher potrebbe avere senso per un lightning talk, ma proprio come le schermate possono essere distribuite su più moduli, ci aspettiamo che questi stessi moduli contribuiscano a un elenco globale di matcher tramite il sistema 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>>
  • Utilizzando questo approccio, i nostri esempi di Hilt, Metro e Koin nel repository degli esempi sono stati aggiornati per includere i deep link.

Deserializzazione con sicurezza dei tipi e serializzatori personalizzati

  • Clara ha menzionato che la nostra UriDeepLinkMatcher utilizza la serializzazione Kotlin per estrarre i campi dalla richiesta di deep link e inserirli nella nostra classe con sicurezza dei tipi, come la classe di sessione che abbiamo mostrato.

  • Come scritto qui, si tratta di una trasformazione piuttosto semplice.

  • L'URI è una stringa.

  • Il campo è una stringa.

  • Wow, è stato facile.

  • Tuttavia, sappiamo che dobbiamo andare oltre le semplici proprietà di stringa.

  • È qui che Kotlin Serialization ci fornisce già una solida base su cui lavorare.

  • Tutti i tipi primitivi, come booleani, numeri interi, numeri in virgola mobile e persino enumerazioni, funzionano immediatamente, non riuscendo automaticamente a trovare una corrispondenza e restituendo null se il valore del deep link non può essere analizzato come quel tipo specifico.

  • Ad esempio, se il parametro ID della nostra sessione fosse effettivamente un numero intero anziché una stringa, il passaggio di una stringa che non può essere analizzata come numero intero non corrisponderebbe.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Analogamente, se vuoi avere argomenti facoltativi, ad esempio per i dati di analisi allegati all'URL web, questi potrebbero essere proprietà della chiave con valori predefiniti.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Come puoi notare, abbiamo cercato di separare la struttura dell'URL web, che potrebbe essere di proprietà di un altro team, dalle classi con sicurezza dei tipi o dalle classi che rappresentano la corrispondenza del deep link nella tua app.

  • Perché so che alcuni di voi stanno gestendo dei deep link assolutamente terribili.

    // "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
  • Ad esempio, potresti avere un'immagine con codifica base64 allegata al deep link.

  • Ad esempio, un blob JSON gigante che hanno deciso di inserire in ogni URL della tua app.

  • Ora esiste un modo per procedere anche quando non puoi modificare l'URL web che ti viene fornito, ma devi comunque estrarre alcune informazioni.

  • Quindi, la prima tecnica consiste nell'estendere la nostra classe DeepLinkSerializer per controllare esattamente come l'argomento viene deserializzato o trasformato da una stringa nella proprietà con sicurezza dei tipi.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer è un wrapper helper per la serializzazione Kotlin che semplifica l'implementazione della classe del serializzatore, perché in questo caso l'input del deserializzatore è sempre una stringa dell'URL web.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Ora, per implementare un DeepLinkSerializer, devi scrivere tre metodi.

  • Per prima cosa, scegli un nome per la serie.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Non è necessario che sia associato a uno spazio dei nomi, ma deve essere sufficientemente univoco affinché la serializzazione di Kotlin sappia quando utilizzare il serializzatore.

  • Successivamente, devi implementare deserialize, a cui viene passata la stringa dall'URL web e che è responsabile della creazione dell'oggetto con sicurezza dei tipi.

    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)
      }
    
       ...
    
    }
  • Infine, implementerai la serializzazione, che è l'approccio opposto e viene utilizzato se la stessa classe di chiavi viene utilizzata in casi come 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)
      }
    }
  • Ora applicherai il tuo serializzatore personalizzato alla proprietà che ha quella classe un po' speciale e il web UriDeepLinkMatcher si occuperà del resto.

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

Pre-elaborazione degli argomenti dell'URL con matcher personalizzati

  • Ma cosa succede se vuoi solo una piccola parte dei dati della stringa base64 o del JSON allegato all'URL web?

  • In questo caso, potrebbe avere senso pre-elaborare alcuni URL web prima che arrivino alla serializzazione Kotlin.

  • Per gestire questi casi, puoi estendere UriDeepLinkMatcher ed eseguire l'override del relativo metodo matchArguments.

  • Ora, forse per il nostro blob JSON, tutto ciò che vogliamo veramente è solo l'ID sessione, come se non ci importasse di tutto il resto.

  • Quindi creiamo la nostra sottoclasse ed eseguiamo l'override di 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>? {
        ...
    }
  • Gli input sono le stringhe non elaborate, ovvero tutti i segnaposto aggiunti dal percorso dell'URI.

  • In questo modo, possiamo pre-elaborare e modificare questi valori prima di chiamare 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)
    }
  • Ora possiamo avere valori semplificati qui e non dobbiamo preoccuparci della serializzazione di Kotlin.

Creazione di back stack sintetici per i deep link

  • Grazie, Ian.

  • Ci auguriamo che non dovrai ricorrere spesso a queste tecniche, ma abbiamo riflettuto molto su come puoi controllare il comportamento di corrispondenza.

  • Ok.

  • Ora sappiamo come ottenere un NavKey con i matcher dei deep link, ma un back stack con solo la chiave di navigazione potrebbe non offrire la migliore esperienza utente.

  • Quindi, per garantire il corretto comportamento del pulsante Indietro, devi utilizzare un back stack sintetico.

  • La parte sintetica deriva dal fatto che il back stack contiene chiavi a cui l'utente non ha mai navigato manualmente, ma è il percorso che avrebbe seguito se lo avesse fatto.

  • Ad esempio, se l'utente fa clic su una notifica relativa a una sessione che sta per iniziare, viene indirizzato direttamente a quella sessione, ma quando attiva il pulsante Indietro del sistema, probabilmente vuoi che torni alla schermata Home prima di uscire dal programma di avvio delle app.

  • La schermata Home diventa parte del back stack sintetico.

  • Per le API di deep link, volevamo bilanciare due requisiti in competizione.

  • Da un lato, sappiamo che deep link o matcher diversi richiedono una logica diversa per la creazione del back stack sintetico.

  • Ad esempio, l'aggiunta di una schermata Home prima di ogni deep link potrebbe funzionare finché non vuoi creare un deep link a una schermata secondaria all'interno di una schermata delle impostazioni.

  • Ora hai bisogno di un livello aggiuntivo nel back stack sintetico per quel deep link specifico.

  • Naturalmente, questo collega il generatore di back stack a ogni matcher di deep link.

  • D'altra parte, sappiamo anche che ogni millisecondo che precede la visualizzazione della schermata corretta costa denaro reale.

  • Quindi non volevamo calcolare un back stack sintetico per ogni risultato di corrispondenza.

  • Volevamo solo la corrispondenza migliore.

  • Quindi, se vogliamo una logica personalizzata per ogni matcher, ma non vogliamo creare un back stack sintetico in modo eager per ogni corrispondenza, cosa facciamo?

  • Quindi, dopo alcuni tentativi, abbiamo trovato un approccio che bilancia entrambi i requisiti.

  • Indipendentemente dal tipo di matcher, puoi migliorarlo chiamando un withBackStack per creare in modo lazy il back stack con il MatchResult specificato.

    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)
  • Quindi, se abbiamo più matcher, ognuno avrà la propria chiamata withBackStack per creare il back stack appropriato per quel NavKey specifico.

  • Il codice più pulito si ottiene se ogni matcher che chiami utilizza withBackStack, che ti fornirà un elenco di matcher del back stack.

  • E quando trovi la corrispondenza migliore, il risultato della corrispondenza che ottieni è un BackStackMatchResult.

  • Si tratta di una sottoclasse di MatchResult che aggiunge una nuova proprietà denominata backStack, ovvero l'intero back stack generato dalla funzione lambda withBackStack.

  • Questo approccio a livelli ci ha permesso di disaccoppiare la logica all'interno dei matcher da qualsiasi logica personalizzata che vuoi utilizzare all'interno della tua funzione lambda withBackStack.

Lightning round, riepilogo e risorse

  • Questo è un lightning talk, quindi purtroppo il tempo a nostra disposizione è quasi scaduto, ma forse possiamo esaminare alcune delle altre funzionalità di Navigation 3 che troverai nelle nostre ricette di Nav 3.

  • Ok.

  • Pertanto, puoi aggiungere filtri a qualsiasi matcher per interrompere la logica di corrispondenza ed escludere rapidamente il matcher in base all'azione, al tipo MIME o ai tuoi filtri personalizzati.

  • Questo avviene prima di qualsiasi analisi dell'URI o serializzazione personalizzata, la serializzazione Kotlin.

  • Offriamo anche un StaticKeyDeepLinkMatcher, specifico per i casi in cui non è necessario eseguire l'analisi per ottenere il NavKey, ma vuoi comunque applicare i filtri.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Questa opzione è perfetta per gestire i deep link solo per azioni o tipi MIME, ad esempio un'app di fotoritocco che può gestire l'apertura di immagini JPEG.

  • A proposito di azione e tipo MIME, puoi allegare qualsiasi RequestExtras personalizzato al tuo DeepLinkRequest, che i filtri personalizzati e le sottoclassi DeepLinkRequest possono utilizzare come parte della loro logica.

  • In questo modo puoi integrare framework di test AB o indicatori esterni.

  • Inoltre, abbiamo un'intera serie di API con sicurezza dei tipi per gli extra.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Inoltre, quasi tutta questa logica si trova nel codice KMP comune di Nav3, quindi tutti i matcher e la logica del back stack possono essere condivisi tra le piattaforme.

  • Quindi, se vuoi esaminare con più calma il modo in cui Navigation 3 supporta i deep link, dai un'occhiata alle nostre ricette di Nav3.

  • Sì, e abbiamo anche integrato le nostre API di deep link nelle nostre ricette di DI, oltre che nella nostra app di punta, NowInAndroid.

  • Grazie per aver partecipato al nostro lightning talk e buona navigazione.

  • Grazie.

Codice

Snippet di codice

Snippet Kotlin al minuto 04:41 in "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Snippet Kotlin al minuto 04:53 in "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Snippet Kotlin al minuto 05:04 in "Deep link API design and URI pattern matching"
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Snippet Kotlin al minuto 05:12 in "Deep link API design and URI pattern matching"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  …
)
Snippet Kotlin al minuto 05:39 in "Deep link API design and URI pattern matching"
// target NavKey
@Serializable data class Session(val id: String) : NavKey

val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
  serializer = serializer<Session>()
)
Snippet Kotlin al minuto 05:48 in "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>()
)
Snippet Kotlin al minuto 06:12 in "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>()
)

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

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

val backStack = listOf(matchResult?.key ?: Home)
Snippet di Kotlin al minuto 07:11 in "Deep link API design and URI pattern matching"
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Snippet Kotlin al minuto 07:48 in "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
)
Snippet Kotlin al minuto 08:04 in "Deep link API design and URI pattern matching"
// 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>>
Snippet di Kotlin al minuto 09:15 in "Deserializzazione sicura dei tipi e serializzatori personalizzati"
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Snippet di Kotlin alle 09:27 in "Deserializzazione sicura dei tipi e serializzatori personalizzati"
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Snippet Kotlin al minuto 09:53 in "Type-safe deserialization and custom serializers"
// "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
Snippet di Kotlin al minuto 10:19 in "Deserializzazione sicura dei tipi e serializzatori personalizzati"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Snippet di Kotlin alle 10:32 in "Type-safe deserialization and custom serializers"
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Snippet Kotlin al minuto 10:51 in "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Snippet Kotlin al minuto 11:01 in "Type-safe deserialization and custom serializers"
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)
  }

   ...

}
Snippet di Kotlin al minuto 11:13 in "Type-safe deserialization and custom serializers"
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)
  }
}
Snippet Kotlin al minuto 11:24 in "Type-safe deserialization and custom serializers"
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}

@Serializable data class Session(
  val id: String,
  @Serializable(BitmapSerializer::class)
  val placeholderImage: Bitmap? = null
) : NavKey
Snippet di Kotlin al minuto 12:09 in "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>? {
    ...
}
Snippet Kotlin al minuto 12:21 in "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)
}
Snippet Kotlin al minuto 14:31 in "Creazione di back stack sintetici per i deep link"
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)
Snippet Kotlin al minuto 15:52 in "Lightning round, summary, and resources"
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Snippet di Kotlin al minuto 16:24 in "Lightning round, summary, and resources"
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}