Apps-Grundlage

Typsichere Deeplinks mit Jetpack Navigation3 erstellen

Lautsprecher

  • Clara Fok

    Softwareentwickler

  • Ian Lake

    Engineering Manager

Info

Über den Vortrag

Die Verwaltung von Deeplinks in modularen Android-Apps kann leicht zu fehlerhaften Routen und unerwarteten Laufzeitproblemen führen.In dieser Session wird die neue typsichere Deeplink-Architektur von Jetpack Navigation3 für Compose vorgestellt, mit der Sie ein robustes und skalierbares Routingsystem erstellen können. Wir sehen uns an, wie Sie vordefinierte APIs für den Deeplink-Abgleich implementieren, benutzerdefinierte Serializer für komplexe typsichere Argumente schreiben und dynamische hierarchische Back Stacks erstellen, die die Navigation für Nutzer vorhersehbar machen.

Zusammenfassung

  • Die Deeplink-APIs von Navigation 3 wandeln eingehende Deeplink-Anfragen in Navigationsschlüssel um, um den anfänglichen Back Stack zu bestimmen. Die KMP-kompatible Klasse DeepLinkRequest kapselt die Eingabe, einschließlich eines Android-Intent-Konstruktors.
  • Ein UriDeepLinkMatcher verwendet die Kotlin-Serialisierung, um URI-Muster abzugleichen und Argumente in typsichere NavKey-Eigenschaften zu deserialisieren. Primitive Typen funktionieren automatisch. Übereinstimmungen schlagen fehl, wenn Werte nicht geparst werden können.
  • Entwickler können DeepLinkSerializer erweitern oder matchArguments überschreiben, um komplexe Deeplink-Strings zu verarbeiten. Durch die Erstellung einer Unterklasse von UriDeepLinkMatcher können Sie Roh-URI-Platzhalter vorverarbeiten, bevor die Kotlin-Serialisierung ausgeführt wird.
  • Wenn Sie withBackStack für Abgleiche aufrufen, werden synthetische Back Stacks für die beste Übereinstimmung verzögert erstellt. So wird das richtige Verhalten des Systems beim Zurückgehen sichergestellt, ohne dass für jede andere gültige Übereinstimmung unnötige Back Stacks berechnet werden müssen.

Ressourcen

Gemini Notebook

Themen

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

Transkript

Einführung und Navigation – 3 Rezepte

  • Hallo, ich bin Ian Lake, Engineering Manager für das Jetpack Architecture-Team.

  • Und ich bin Clara Fok, die technische Leiterin für Nav 3.

  • Heute möchten wir über eine Funktion sprechen, die mir sehr am Herzen liegt: Deeplinks.

  • Ich mag sie sogar so sehr, dass ich die Implementierung für Nav2 und Nav3 im Rahmen der Version 1.2 von Nav3 vorgenommen habe.

  • Ich glaube, du hast es sogar dreimal gemacht, weil wir es auch in den Nav 3-Rezepten implementiert haben.

  • Ja, das stimmt.

  • Wir haben die API mehrfach überarbeitet, damit sie sich gut in den Compose-First-Ansatz von Nav3 einfügt.

  • Ein Großteil davon begann mit unserem Fokus auf Rezepte.

  • Ja, eines unserer Hauptziele für Navigation 3 war, dass die Navigation nicht im Weg ist.

  • Wenn Sie eine besonders anspruchsvolle Funktion entwickeln möchten, sollte es nicht erforderlich sein, dass wir einen Feature Request stellen, um dies zu ermöglichen.

  • Wenn wir also die Frage „Wie mache ich das?“ erhalten,

  • Oder: „Wie gehe ich dieses Problem überhaupt an?“

  • Anstatt von Anfang an eine benutzerdefinierte API zu erstellen, entwickeln wir ein Rezept, also eine Anleitung, in der erklärt wird, wie Sie diese Funktion Ihrer App hinzufügen können, ohne dass Änderungen an Navigation 3 erforderlich sind.

  • Dieser Ansatz, Dinge zu zeigen, anstatt sie nur zu erklären, hat uns dazu veranlasst, das Nav3-Repository mit Rezepten auf GitHub zu erstellen.

  • Inzwischen enthält es über 35 Rezepte für gängige Anwendungsfälle wie mehrere Back Stacks und Ansichten am unteren Rand. Dort finden Sie den gesamten Code, den Sie für die Implementierung in Ihrer App benötigen.

  • Das bedeutete, dass wir schon für die Version 1.0 von Nav3 im November 2025 eine Anleitung für die Implementierung von Deeplinks hatten, weil wir wussten, dass das für viele von euch wichtig ist.

  • Wenn wir jedoch feststellen, dass für ein Rezept eine Reihe von Hilfsmethoden erforderlich sind, die Sie in Ihre App kopieren und einfügen müssen, ziehen wir einen Prozess namens „Graduation“ in Betracht. Dabei nehmen wir diese Hilfsmethoden und überlegen, wie sie aussehen würden, wenn sie Teil von Navigation 3 selbst wären.

  • So ist sichergestellt, dass Entwickler, die diesen Code bereits kopiert und eingefügt haben, keine Probleme haben.

  • Sie können diesen Code behalten.

  • Wenn Sie benutzerdefinierte Lösungen haben, die funktionieren, ist das großartig.

  • Es gibt keine erzwungenen Migrationen.

  • Wenn es sich jedoch um ein ganz neues Problem oder eine neue Anforderung handelt, erhalten Sie eine einfache, geprüfte Lösung, die Sie direkt als Teil der Navigation verwenden können.

  • Aber das Beste ist für mich persönlich, dass wir nach dem Hinzufügen dieses Hilfsprogramms unsere Rezepte aktualisieren und den Dopaminkick bekommen, wenn wir Code löschen.

  • So hat jeder, der sich die Rezepte für den empfohlenen Ansatz für dieses Problem ansieht, die aktuellste Lösung.

  • Natürlich ist man im Nachhinein immer schlauer. Der Prozess der Übernahme in die Standardbibliothek gibt uns die Möglichkeit, uns die Hilfsfunktionen, die wir in den Rezepten erstellt haben, genau anzusehen und zu fragen: „Sind das wirklich die besten APIs dafür?“

  • In diesem Fall haben wir die Möglichkeit, diese Dinge zu verbessern, wenn wir sie in die Mediathek verschieben.

  • Das war bei Deeplinks definitiv der Fall.

Deeplink-API-Design und URI-Musterabgleich

  • Danke, Ian.

  • Konzeptionell sind sowohl die Rezepte als auch unsere endgültigen APIs sehr ähnlich, da Sie in Nav 3 den Back Stack besitzen.

  • Das bedeutet, dass der Schwerpunkt unserer Deeplink-APIs darauf liegt, herauszufinden, wie der anfängliche Back Stack aussehen sollte, insbesondere, welchen Bildschirm Nutzer sehen, wenn sie auf einen Deeplink klicken, um Ihre App zu öffnen.

  • Was passiert aber, wenn der Nutzer auf diesen Web-URI klickt?

  • Unabhängig davon, ob Sie Nav3 verwenden oder nicht, müssen Sie im Android-Framework Intent-Filter für jeden Deeplink deklarieren, den Ihre App unterstützt.

  • Interessant wird es, wenn dieser Deeplink die Aktivität startet.

  • Hier ist die Eingabe der Intent, mit dem Ihre App gestartet wird, also die Aktivität, die gestartet wurde.

  • Der Deeplink kann jedoch viele Formen annehmen.

  • Sie können sie beispielsweise manuell für einen Test erstellen oder die Kotlin Multiplatform-Unterstützung nutzen.

  • Wir haben die Deeplink-Eingabe also in einer KMP-kompatiblen DeepLinkRequest-Klasse gekapselt.

  • Unter Android gibt es einen DeepLinkRequest-Konstruktor, der einen Android-Intent akzeptiert.

  • Als Nächstes möchten wir herausfinden, wie wir bei einer Anfrage wissen, welche NavKey in den Back Stack aufgenommen werden müssen.

  • Hier kommt unsere DeepLinkMatcher-Klasse ins Spiel.

  • Sie nimmt eine Anfrage entgegen und gibt ein MatchResult aus, das die mit der Übereinstimmung verknüpfte NavKey enthält.

  • Speziell für Web-URIs haben wir eine UriDeepLinkMatcher, die die gesamte Logik enthält, die erforderlich ist, um eine bestimmte Anfrage in eine MatchResult umzuwandeln.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Der erste Parameter, den ein UriDeepLinkMatcher annimmt, ist ein URI-Muster.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Angenommen, Sie entwickeln eine ADS-App und möchten dem Startbildschirm einen Deeplink hinzufügen.

  • Hier ist das Muster ein genauer URI. Wir können das HTTP-Schema weglassen, um automatisch sowohl HTTP als auch HTTPS abzugleichen.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Wenn wir jedoch einen Deeplink zu einer bestimmten ADS-Sitzung erstellen möchten, enthält dieser Deeplink Informationen, die wir extrahieren müssen, nämlich die Sitzungs-ID.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • In diesem Fall werden in unserem Muster geschweifte Klammern als Platzhalter für Informationen verwendet, die wir aus dem Deeplink extrahieren müssen.

  • Es gibt bereits eine praktische Bibliothek, mit der Sie jede Art von Eingabe in Klasseninstanzen konvertieren können: Kotlin Serialization.

  • Unser UriDeepLinkMatcher verwendet also einen Kotlin-Decoder, um einen Eingabe-URI zu deserialisieren und eine NavKey-Instanz auszugeben.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Der Decoder muss wissen, mit welcher Klasse er arbeiten soll. Sie müssen also auch einen Serializer für NavKey an den Matcher übergeben.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Damit der Decoder die Platzhalter den Eigenschaften des Schlüssels richtig zuordnen kann, muss der Name des Platzhalters mit den Eigenschaftsnamen übereinstimmen.

  • In diesem Beispiel lautet der Attributname „ID“ und entspricht dem ID-Parameter der Sitzung.

  • Nachdem wir nun die UriDeepLinkMatcher haben, müssen wir mit dem eingehenden Deeplink einen DeepLinkRequest erstellen.

    // 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)
  • Anschließend wird die Abgleichfunktion aufgerufen und die Anfrage übergeben.

  • Dadurch erhalten Sie einen Nullable-Wert vom Typ MatchResult, der den NavKey enthält, in unserem Beispiel also den Sitzungsschlüssel. Wenn das Ergebnis null ist, können wir einfach den Startbildschirm als Standard verwenden.

  • Mehrere Deeplinks sollten jetzt als mehrere Matcher geschrieben werden: ein Matcher für jeden Deeplink.

  • Hier haben wir beispielsweise zwei URI-Muster für unseren Sitzungsschlüssel: eines für ein älteres Newsletter-Muster und eines für einen regulären Web-URI.

  • Außerdem haben wir einen Deeplink zum ADS-Startbildschirm, also fügen wir dafür einen dritten Matcher hinzu.

  • Und alle Matcher würden derselben Liste hinzugefügt, die wir durchlaufen.

  • Jetzt wird unsere Übereinstimmung zu einer Schleife über alle Abgleiche in unserer Liste, wobei wir für jeden Abgleich aufrufen, um alle Abgleiche zu finden, die mit der Anfrage übereinstimmen.

  • Dazu können wir mit mapNotNull die nicht übereinstimmenden Werte herausfiltern und eine Liste mit nur den gültigen Übereinstimmungen erhalten.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • In den meisten Fällen gibt es wahrscheinlich nur eine Übereinstimmung, die nicht null ist, aber es kann auch mehrere gültige Übereinstimmungen geben.

  • Angenommen, wir führen einen speziellen Keynote-Bildschirm ein, der dasselbe URI-Muster für die Sitzung verwendet, aber anstelle des ID-Platzhalters einen statischen Keynote-Pfad hat.

  • In diesem Fall kann dieselbe Anfrage mit beiden Abgleichern übereinstimmen: dem Sitzungsschlüssel, bei dem die ID „keynote“ ist, und der Keynote NavKey.

  • Da MatchResult „Comparable“ implementiert, können wir die beste Übereinstimmung finden, indem wir alle gültigen Übereinstimmungen in der Liste mit der Funktion „max“ vergleichen, oder mit maxOrNull, um den Fall zu behandeln, dass keine Deeplinks mit der Anfrage übereinstimmen.

    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
    )
  • Eine statische Liste von Matchern mag für einen Lightning Talk sinnvoll sein, aber genau wie Ihre Bildschirme auf mehrere Module verteilt sein können, erwarten wir, dass diese Module über Ihr DI-System zu einer globalen Liste von Matchern beitragen.

    // 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>>
  • Mit diesem Ansatz wurden unsere Hilt-, Metro- und Koin-Rezepte in unserem Rezepte-Repository aktualisiert, um Deeplinks einzubinden.

Typsichere Deserialisierung und benutzerdefinierte Serializer

  • Clara hat erwähnt, dass unsere UriDeepLinkMatcher Kotlin Serialization verwendet, um Felder aus der Deeplink-Anfrage zu extrahieren und in unsere typsichere Klasse umzuwandeln, wie die von uns gezeigte Sitzungsklasse.

  • Wie hier beschrieben, ist dies eine ziemlich einfache Transformation.

  • Der URI ist ein String.

  • Das Feld ist ein String.

  • Wow, das war einfach.

  • Uns war jedoch klar, dass wir über String-Attribute hinausgehen mussten.

  • Hier bietet die Kotlin-Serialisierung bereits eine solide Grundlage.

  • Alle primitiven Typen wie boolesche Werte, Ganzzahlen, Gleitkommazahlen und sogar Enums funktionieren sofort. Wenn der Deeplink-Wert nicht als dieser spezifische Typ geparst werden kann, schlägt der Abgleich automatisch fehl und es wird „null“ zurückgegeben.

  • Wenn der ID-Parameter unserer Sitzung beispielsweise eine Ganzzahl statt eines Strings wäre, würde die Übergabe eines Strings, der nicht in eine Ganzzahl geparst werden kann, zu einem Fehler führen.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Wenn Sie optionale Argumente verwenden möchten, z. B. für Analysedaten, die an Ihre Web-URL angehängt sind, können diese als Attribute für Ihren Schlüssel mit Standardwerten verwendet werden.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Wie Sie sehen, haben wir uns sehr bemüht, die Struktur Ihrer Web-URL, die möglicherweise einem anderen Team gehört, von den typsicheren Klassen oder Klassen zu entkoppeln, die die Deeplink-Übereinstimmung in Ihrer App darstellen.

  • Weil ich weiß, dass einige von Ihnen mit absolut schrecklichen Deeplinks zu tun haben.

    // "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
  • Vielleicht ist an Ihren Deeplink ein base64-codiertes Bild angehängt.

  • Vielleicht ein riesiges JSON-Blob, das sie in jede URL Ihrer App einfügen wollten.

  • Es gibt eine Möglichkeit, auch wenn Sie die Web-URL, die Sie erhalten, nicht ändern können, aber dennoch Informationen daraus extrahieren müssen.

  • Die erste Technik besteht darin, die Klasse DeepLinkSerializer zu erweitern, um genau zu steuern, wie dieses Argument deserialisiert oder von einem String in Ihre typsichere Property umgewandelt wird.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer ist ein Hilfs-Wrapper für die Kotlin-Serialisierung, der die Implementierung der Serializer-Klasse vereinfacht, da die Deserializer-Eingabe in diesem Fall immer ein String aus Ihrer Web-URL ist.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Um einen DeepLinkSerializer zu implementieren, schreiben Sie drei Methoden.

  • Zuerst wählen Sie einen Seriennamen aus.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Es muss kein Namespace verwendet werden, aber der Name muss eindeutig genug sein, damit die Kotlin-Serialisierung weiß, wann dein Serializer verwendet werden soll.

  • Als Nächstes müssen Sie „deserialize“ implementieren. Dieser Methode wird der String aus der Web-URL übergeben und sie ist für die Erstellung des typsicheren Objekts verantwortlich.

    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)
      }
    
       ...
    
    }
  • Zuletzt implementieren Sie die Serialisierung, was der umgekehrte Ansatz ist. Dies wird verwendet, wenn dieselbe Schlüsselklasse in etwa rememberNavBackStack Fällen verwendet wird.

    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)
      }
    }
  • Jetzt wenden Sie Ihren benutzerdefinierten Serializer auf die Property an, die diese etwas spezielle Klasse hat, und Web-UriDeepLinkMatcher kümmert sich um den Rest.

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

URL-Argumente mit benutzerdefinierten Matchern vorverarbeiten

  • Aber was ist, wenn Sie nur einen kleinen Teil der Daten aus diesem Base64-String oder dem JSON-Objekt benötigen, das an Ihre Web-URL angehängt wurde?

  • In diesem Fall kann es sinnvoll sein, einen Teil der Web-URL vorzuverarbeiten, bevor sie überhaupt an die Kotlin-Serialisierung übergeben wird.

  • In solchen Fällen kannst du UriDeepLinkMatcher erweitern und die Methode matchArguments überschreiben.

  • Vielleicht möchten wir für unser JSON-Blob nur die Sitzungs-ID und der Rest ist uns egal.

  • Wir erstellen also unsere Unterklasse und überschreiben 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>? {
        ...
    }
  • Die Eingaben sind diese Rohstrings, alle Platzhalter, die aus Ihrem URI-Pfad hinzugefügt wurden.

  • So können wir diese Werte vorverarbeiten und bearbeiten, bevor wir super.matchArguments aufrufen.

    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)
    }
  • Jetzt können wir hier vereinfachte Werte verwenden und müssen uns überhaupt nicht mehr mit der Kotlin-Serialisierung herumschlagen.

Synthetische Back Stacks für Deeplinks erstellen

  • Vielen Dank, Ian.

  • Hoffentlich müssen Sie diese Techniken nicht oft anwenden, aber wir haben uns viele Gedanken darüber gemacht, wie Sie das Abgleichverhalten steuern können.

  • Okay.

  • Wir wissen jetzt, wie wir mit den Deeplink-Abgleichern ein NavKey erhalten. Ein Back Stack, der nur den Navigationsschlüssel enthält, bietet jedoch möglicherweise nicht die beste Nutzererfahrung.

  • Damit das Zurück-Verhalten wie gewünscht funktioniert, benötigen Sie einen synthetischen Back Stack.

  • Der synthetische Teil ergibt sich daraus, dass der Back Stack Schlüssel enthält, zu denen der Nutzer nie manuell navigiert ist. Es ist jedoch der Pfad, den er genommen hätte, wenn er manuell dorthin navigiert wäre.

  • Wenn ein Nutzer beispielsweise auf eine Benachrichtigung für eine bevorstehende Sitzung klickt, wird er direkt zu dieser Sitzung weitergeleitet. Wenn er dann die Zurück-Taste drückt, sollte er wahrscheinlich zum Startbildschirm zurückkehren, bevor er zum Launcher gelangt.

  • Dieser Startbildschirm wird Teil des synthetischen Back Stack.

  • Bei Deeplink-APIs wollten wir zwei konkurrierende Anforderungen in Einklang bringen.

  • Einerseits wissen wir, dass für verschiedene Deeplinks oder verschiedene Matcher unterschiedliche Logiken zum Erstellen des synthetischen Back Stacks erforderlich sind.

  • Wenn Sie beispielsweise vor jedem Deeplink einen Startbildschirm hinzufügen, funktioniert das möglicherweise, bis Sie einen Deeplink zu einem Unterbildschirm innerhalb eines Einstellungsbildschirms erstellen möchten.

  • Jetzt benötigen Sie eine zusätzliche Ebene in Ihrem synthetischen Back Stack für diesen bestimmten Deeplink.

  • Dadurch wird der Back Stack Builder natürlich an jeden Deeplink-Matcher gebunden.

  • Andererseits wissen wir auch, dass jede Millisekunde, die bis zur Anzeige des richtigen Bildschirms vergeht, Sie echtes Geld kostet.

  • Wir wollten also nicht für jedes Übereinstimmungsergebnis einen synthetischen Back Stack berechnen.

  • Wir wollten nur die beste Übereinstimmung.

  • Wenn wir also eine benutzerdefinierte Logik für jeden Matcher haben möchten, aber nicht für jede Übereinstimmung einen synthetischen Back Stack erstellen wollen, was können wir dann tun?

  • Nach einigen Fehlversuchen haben wir einen Ansatz gefunden, der beide Anforderungen erfüllt.

  • Unabhängig davon, welche Art von Matcher Sie haben, können Sie diesen bestimmten Matcher erweitern, indem Sie einen withBackStack aufrufen, um den Back Stack mit dem angegebenen MatchResult verzögert zu erstellen.

    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)
  • Wenn wir also mehrere Matcher haben, hat jeder einen eigenen withBackStack-Aufruf, um den entsprechenden Back Stack für das jeweilige NavKey zu erstellen.

  • Der Code ist am saubersten, wenn jeder aufgerufene Matcher den withBackStack verwendet, der eine Liste von Back-Stack-Matchern zurückgibt.

  • Wenn Sie die beste Übereinstimmung finden, ist das zurückgegebene Ergebnis ein BackStackMatchResult.

  • Dies ist eine Unterklasse von MatchResult, die ein neues Attribut namens backStack hinzufügt. Dieses Attribut ist der gesamte Back Stack, der von Ihrem withBackStack-Lambda generiert wird.

  • Durch diesen mehrschichtigen Ansatz konnten wir die Logik innerhalb der Matcher von der benutzerdefinierten Logik entkoppeln, die Sie in Ihrem withBackStack-Lambda verwenden möchten.

Blitzrunde, Zusammenfassung und Ressourcen

  • Da dies ein Lightning Talk ist, ist unsere Zeit leider fast um. Aber vielleicht können wir noch ein paar der anderen Funktionen von Navigation 3 durchgehen, die ihr in unseren Navigation 3-Rezepten findet.

  • Okay.

  • Sie können also jedem Abgleicher Filter hinzufügen, um die Abgleichslogik zu verkürzen und den Abgleicher schnell anhand von Aktion, MIME-Typ oder Ihren eigenen benutzerdefinierten Filtern auszuschließen.

  • Und das geschieht, bevor ein URI-Parsing oder eine benutzerdefinierte Serialisierung (Kotlin-Serialisierung) erfolgt.

  • Wir bieten auch eine StaticKeyDeepLinkMatcher an, die speziell für Fälle gedacht ist, in denen Sie kein Parsing durchführen müssen, um Ihre NavKey zu erhalten, aber dennoch Filter anwenden möchten.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Dies ist ideal für die Verarbeitung von Deeplinks, die nur auf Aktionen oder MIME-Typen basieren, z. B. eine Fotobearbeitungs-App, die JPEG-Bilder öffnen kann.

  • Apropos Aktion und MIME-Typ: Sie können an Ihre DeepLinkRequest beliebige benutzerdefinierte RequestExtras anhängen, die Ihre benutzerdefinierten Filter und DeepLinkRequest-Unterklassen als Teil ihrer Logik verwenden können.

  • So können Sie AB-Test-Frameworks oder externe Signale einbinden.

  • Und wir haben auch typsichere APIs für die Extras.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Und fast die gesamte Logik befindet sich im gemeinsamen KMP-Code von Nav3, sodass alle Matcher und die Back-Stack-Logik plattformübergreifend genutzt werden können.

  • Wenn Sie sich also genauer ansehen möchten, wie Navigation 3 Deeplinks unterstützt, sehen Sie sich unsere Nav3-Rezepte an.

  • Ja, und wir haben unsere Deeplink-APIs auch in unsere DI-Rezepte sowie in unsere Hero-App „NowInAndroid“ eingebunden.

  • Vielen Dank für eure Teilnahme an unserem Lightning Talk und viel Spaß beim Navigieren.

  • Vielen Dank.

Code

Code-Snippets

Kotlin-Snippet bei 04:41 in „Deeplink API Design and URI Pattern Matching“
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Kotlin-Snippet bei 04:53 in „Deeplink API Design and URI Pattern Matching“
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Kotlin-Snippet bei 05:04 in „Deeplink API Design and URI Pattern Matching“
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Kotlin-Snippet bei 05:12 in „Deeplink 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}"),
  …
)
Kotlin-Snippet bei 05:39 in „Deeplink 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>()
)
Kotlin-Snippet bei 05:48 in „Deeplink 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>()
)
Kotlin-Snippet bei 06:12 in „Deeplink 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)
Kotlin-Snippet bei 07:11 in „Deeplink-API-Design und URI-Musterabgleich“
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Kotlin-Snippet bei 07:48 in „Deeplink 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
)
Kotlin-Snippet bei 08:04 in „Deeplink 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>>
Kotlin-Snippet bei 09:15 in „Type-safe deserialization and custom serializers“
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Kotlin-Snippet bei 09:27 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Kotlin-Snippet bei 09:53 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
// "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
Kotlin-Snippet bei 10:19 in „Type-safe deserialization and custom serializers“
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Kotlin-Snippet bei 10:32 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Kotlin-Snippet bei 10:51 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Kotlin-Snippet bei 11:01 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
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)
  }

   ...

}
Kotlin-Snippet bei 11:13 in „Typsichere Deserialisierung und benutzerdefinierte Serializer“
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)
  }
}
Kotlin-Snippet bei 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
Kotlin-Snippet bei 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>? {
    ...
}
Kotlin-Snippet bei 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)
}
Kotlin-Snippet bei 14:31 in „Synthetische Back Stacks für Deeplinks erstellen“
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)
Kotlin-Snippet bei 15:52 in „Lightning Round, Zusammenfassung und Ressourcen“
@Serializable object ImagesKey: NavKey

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

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