Podstawy aplikacji

Tworzenie precyzyjnych linków z zabezpieczeniami wpisywania za pomocą Jetpack Navigation3

Głośniki

  • Clara Fok

    Inżynier oprogramowania

  • Ian Lake

    Menedżer ds. inżynierii

Informacje

Informacje o wystąpieniu

Zarządzanie precyzyjnymi linkami w modułowych aplikacjach na Androida może łatwo prowadzić do uszkodzonych ścieżek i niespodzianek w czasie działania.Podczas tej sesji omówimy nową architekturę precyzyjnych linków z zabezpieczeniami wpisywania w Jetpack Navigation 3 dla Compose, która pomoże Ci stworzyć solidny i skalowalny system routingu. Omówimy, jak wdrażać gotowe interfejsy API dopasowywania precyzyjnych linków, pisać niestandardowe serializatory dla złożonych argumentów z zabezpieczeniami wpisywania i tworzyć dynamiczne hierarchiczne stosy wsteczne, które zapewniają przewidywalną nawigację użytkownika.

Podsumowanie

  • Interfejsy API precyzyjnych linków w Navigation 3 przekształcają przychodzące żądania precyzyjnych linków w klucze nawigacyjne, aby określić początkowy stos wsteczny. Zgodna z KMP klasa DeepLinkRequest zawiera dane wejściowe, w tym konstruktor intencji Androida.
  • UriDeepLinkMatcher używa serializacji Kotlin do dopasowywania wzorców identyfikatorów URI i deserializacji argumentów do właściwości NavKey z zabezpieczeniami wpisywania. Typy podstawowe działają automatycznie, a dopasowania nie są możliwe, jeśli nie można przeanalizować wartości.
  • Deweloperzy mogą rozszerzyć DeepLinkSerializer lub zastąpić matchArguments, aby obsługiwać złożone ciągi precyzyjnych linków. Tworzenie podklasy UriDeepLinkMatcher umożliwia wstępne przetwarzanie nieprzetworzonych obiektów zastępczych identyfikatorów URI przed uruchomieniem serializacji Kotlin.
  • Wywołanie metody withBackStack na dopasowaniach powoduje leniwe tworzenie syntetycznych stosów wstecznych w celu uzyskania najlepszego dopasowania. Zapewnia to prawidłowe działanie systemu bez obliczania niepotrzebnych stosów wstecznych dla każdego innego prawidłowego dopasowania.

Zasoby

Gemini Notebook

Tematy

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

Transkrypcja

Pobierz transkrypcję

Wprowadzenie i nawigacja (3 przepisy)

  • Cześć, jestem Ian Lake, kierownik ds. inżynierii w zespole Jetpack Architecture.

  • A ja jestem Clara Fok, kierowniczka techniczna Nav 3.

  • Dzisiaj chcemy porozmawiać o funkcji, która jest mi bardzo bliska, czyli o precyzyjnych linkach.

  • Tak bardzo mi się podobają, że wdrożyłem je zarówno w Nav2, jak i Nav3 w ramach wersji 1.2 Nav3.

  • Myślę, że udało Ci się to zrobić 3 razy, ponieważ wdrożyliśmy to też w przepisach Nav 3.

  • Tak, to prawda.

  • Wprowadziliśmy wiele zmian w interfejsie API, aby upewnić się, że jest on zgodny z podejściem Nav3 opartym na Compose.

  • Wiele z tych zmian zaczęło się od naszego nacisku na przepisy.

  • Tak, jednym z naszych głównych celów w przypadku Nawigacji 3 było to, aby nie przeszkadzała ona użytkownikom.

  • Jeśli chcesz stworzyć szczególnie ambitną funkcję, nie powinno to wymagać od nas prośby o dodanie funkcji, aby było to możliwe.

  • Kiedy więc pojawia się pytanie: „Jak to zrobić?”,

  • albo „Jak w ogóle podejść do tego problemu?”.

  • Zamiast od razu tworzyć niestandardowy interfejs API, tworzymy przepis, czyli przewodnik, który wyjaśnia, jak dodać tę funkcję do aplikacji bez konieczności wprowadzania jakichkolwiek zmian w Navigation 3.

  • To właśnie ten styl prezentowania, a nie opowiadania, skłonił nas do utworzenia repozytorium przepisów Nav3 na GitHubie.

  • Zawiera teraz ponad 35 przepisów na typowe przypadki użycia, takie jak wiele stosów wstecznych i plansze dolne, które zawierają cały kod potrzebny do wdrożenia ich w aplikacji.

  • Oznacza to, że nawet w przypadku wersji 1.0 Nav3, która została wydana w listopadzie 2025 r., mieliśmy już przepis na implementację precyzyjnych linków, ponieważ wiedzieliśmy, że jest to ważne dla wielu z Was.

  • Gdy jednak zdajemy sobie sprawę, że przepis wymaga wielu metod pomocniczych, które trzeba skopiować i wkleić do aplikacji, rozważamy proces, który nazywamy „ukończeniem”. Bierzemy te metody pomocnicze i mówimy: „W porządku, jak by to wyglądało, gdyby były częścią samej nawigacji 3?”.

  • Dzięki temu deweloperzy, którzy już skopiowali i wkleili ten kod, nie będą mieli problemów.

  • Możesz zachować ten kod.

  • Jeśli masz jakieś niestandardowe rozwiązania, które działają, to świetnie.

  • Nie masz żadnych wymuszonych migracji.

  • Jeśli jednak jest to dla Ciebie zupełnie nowy problem lub nowe wymaganie, otrzymujesz proste, sprawdzone rozwiązanie, które możesz wykorzystać bezpośrednio w ramach nawigacji.

  • Ale najlepsze jest to, że po dodaniu tej funkcji pomocniczej wracamy do naszych przepisów i aktualizujemy je, co daje nam zastrzyk dopaminy, gdy usuwamy kod.

  • Dzięki temu każdy, kto szuka przepisów na zalecane podejście do tego problemu, ma najbardziej aktualne rozwiązanie.

  • Oczywiście po fakcie łatwo jest oceniać, a proces przechodzenia na wyższy poziom daje nam możliwość przyjrzenia się tym pomocnikom, których stworzyliśmy w przepisach, i zadania sobie pytania: „Czy to naprawdę najlepsze interfejsy API do tego celu?”.

  • W tym przypadku daje nam to możliwość ulepszenia tych elementów podczas przenoszenia ich do biblioteki.

  • Tak było też w przypadku precyzyjnych linków.

Projekt interfejsu API precyzyjnych linków i dopasowywanie wzorców identyfikatorów URI

  • Dziękuję, Ian.

  • Koncepcyjnie przepisy i nasze ostateczne interfejsy API są bardzo podobne, ponieważ w Nav 3 to Ty zarządzasz stosem wstecznym.

  • Oznacza to, że nasze interfejsy API precyzyjnych linków skupiają się na określeniu, jaki powinien być początkowy stos wsteczny, a konkretnie, jaki ekran widzą użytkownicy, gdy klikną precyzyjny link, aby wejść do aplikacji.

  • Ale co się stanie, gdy użytkownik kliknie ten identyfikator URI?

  • Niezależnie od tego, czy używasz Nav3, platforma Android wymaga zadeklarowania filtrów intencji dla każdego precyzyjnego linku obsługiwanego przez aplikację.

  • Teraz przejdźmy do tego, co dzieje się, gdy precyzyjny link uruchamia Activity.

  • W tym przypadku dane wejściowe to intencja, z którą uruchamiana jest aplikacja, czyli aktywność, która została rozpoczęta.

  • Jednak precyzyjny link może przybierać różne formy.

  • Możesz na przykład utworzyć go ręcznie na potrzeby testu lub skorzystać z obsługi Kotlin Multiplatform.

  • Dlatego umieściliśmy dane wejściowe precyzyjnego linku w klasie DeepLinkRequest zgodnej z KMP.

  • W przypadku Androida mamy konstruktor DeepLinkRequest, który przyjmuje intencję Androida.

  • Następnie musimy dowiedzieć się, jak na podstawie żądania określić, co NavKey musi zostać umieszczone na stosie wstecznym.

  • Tu właśnie przydaje się nasza klasa DeepLinkMatcher.

  • Przyjmuje żądanie i zwraca MatchResult, który zawiera NavKey powiązany z dopasowaniem.

  • W przypadku identyfikatorów URI w internecie mamy UriDeepLinkMatcher, który zawiera całą logikę potrzebną do przekształcenia danego żądania w MatchResult.

    val matcher = UriDeepLinkMatcher(
      ...,
      ...
    )
  • Pierwszym parametrem, który przyjmuje UriDeepLinkMatcher, jest wzorzec identyfikatora URI.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri(...),
      ...
    )
  • Załóżmy, że tworzysz aplikację ADS i chcesz dodać precyzyjny link do ekranu głównego.

  • W tym przypadku wzorzec jest dokładnym identyfikatorem URI, a schemat HTTP możemy pominąć, aby automatycznie dopasowywać zarówno HTTP, jak i HTTPS.

    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/home"),
      ...
    )
  • Jeśli jednak chcemy utworzyć precyzyjny link do konkretnej sesji reklamowej, ten link zawiera informacje, które musimy wyodrębnić, czyli identyfikator sesji.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      …
    )
  • W tym przypadku nasz wzorzec używa nawiasów klamrowych jako obiektów zastępczych dla informacji, które musimy wyodrębnić z precyzyjnego linku.

  • Okazuje się, że istnieje już przydatna biblioteka do konwertowania dowolnego rodzaju danych wejściowych na instancje klas, czyli Kotlin Serialization.

  • Dlatego UriDeepLinkMatcher wykorzystuje dekoder Kotlin do deserializacji wejściowego identyfikatora URI i wygenerowania instancji NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Dekoder musi wiedzieć, z jaką klasą ma pracować, więc do dopasowania musisz też przekazać serializator dla NavKey.

    // target NavKey
    @Serializable data class Session(val id: String) : NavKey
    
    val matcher = UriDeepLinkMatcher(
      uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
      serializer = serializer<Session>()
    )
  • Aby pomóc dekoderowi prawidłowo dopasować obiekty zastępcze do właściwości klucza, nazwa obiektu zastępczego musi odpowiadać nazwom właściwości.

  • W tym przykładzie nazwa właściwości to ID, co odpowiada parametrowi ID sesji.

  • Mamy już UriDeepLinkMatcher, więc musimy utworzyć DeepLinkRequest z przychodzącym precyzyjnym linkiem.

    // 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)
  • Następnie dopasowanie jest wykonywane przez wywołanie funkcji dopasowania i przekazanie naszego żądania.

  • W ten sposób otrzymasz obiekt MatchResult, który może mieć wartość null i zawiera NavKey, czyli w naszym przykładzie klucz sesji. Jeśli wynik ma wartość null, możemy po prostu domyślnie wyświetlić ekran główny.

  • Teraz wiele precyzyjnych linków powinno być zapisanych jako wiele dopasowań: po jednym dopasowaniu dla każdego precyzyjnego linku.

  • Na przykład tutaj mamy 2 wzorce identyfikatorów URI dla klucza sesji: jeden dla starszego wzorca newslettera i jeden dla zwykłego identyfikatora URI w internecie.

  • Mamy też precyzyjny link do ekranu głównego ADS, więc dodajemy do niego trzeci dopasowujący element.

  • Wszystkie dopasowania zostaną dodane do tej samej listy, którą będziemy iterować.

  • Teraz nasze dopasowanie staje się pętlą obejmującą wszystkie dopasowania na liście, w której wywołujemy dopasowanie dla każdego z nich, aby znaleźć wszystkie dopasowania pasujące do żądania.

  • Aby to zrobić, możemy użyć funkcji mapNotNull, aby odfiltrować niepasujące elementy i uzyskać listę tylko prawidłowych dopasowań.

    val matchers = listOf(...)
    
    // gets all non-null MatchResult
    val matchResults = matchers.mapNotNull { matcher ->
      matcher.match(request)
    }
  • W większości przypadków na liście będzie prawdopodobnie tylko jedno dopasowanie, ale zdarzają się sytuacje, w których może być ich więcej.

  • Załóżmy na przykład, że wprowadzamy specjalny ekran prezentacji, który używa tego samego wzorca identyfikatora URI sesji, ale zamiast obiektu zastępczego identyfikatora ma statyczną ścieżkę prezentacji.

  • W tym przypadku to samo żądanie może pasować do obu dopasowań: klucza sesji, w którym identyfikator jest równy keynote, i keynote NavKey.

  • Ponieważ MatchResult implementuje interfejs Comparable, możemy znaleźć najlepsze dopasowanie, porównując wszystkie prawidłowe dopasowania na liście za pomocą funkcji max lub maxOrNull, aby obsłużyć przypadek, w którym nie było żadnych precyzyjnych linków pasujących do żądania.

    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
    )
  • Statyczna lista dopasowań może być przydatna w przypadku krótkiej prelekcji, ale tak jak ekrany mogą być rozproszone w wielu modułach, tak samo te moduły mogą być częścią globalnej listy dopasowań w systemie 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>>
  • Dzięki temu podejściu nasze przepisy na Hilt, Metro i Koin w repozytorium przepisów zostały zaktualizowane i zawierają precyzyjne linki.

Bezpieczna deserializacja i niestandardowe serializatory

  • Clara wspomniała, że nasza UriDeepLinkMatcher używa serializacji Kotlin do wyodrębniania pól z żądania precyzyjnego linku i tworzenia z nich klasy bezpiecznej pod względem typu, takiej jak pokazana klasa sesji.

  • W tym przypadku jest to dość prosta transformacja.

  • Identyfikator URI jest ciągiem znaków.

  • Pole jest ciągiem znaków.

  • To było proste.

  • Zdawaliśmy sobie jednak sprawę, że musimy wyjść poza właściwości ciągów znaków.

  • W tym przypadku serializacja w języku Kotlin zapewnia nam solidne podstawy do pracy.

  • Wszystkie typy podstawowe, takie jak wartości logiczne, liczby całkowite, liczby zmiennoprzecinkowe, a nawet typy wyliczeniowe, działają od razu. Jeśli wartość precyzyjnego linku nie może zostać przeanalizowana jako dany typ, dopasowanie automatycznie kończy się niepowodzeniem i zwracana jest wartość null.

  • Jeśli na przykład parametr identyfikatora sesji byłby liczbą całkowitą, a nie ciągiem znaków, przekazanie ciągu znaków, którego nie można przeanalizować jako liczby całkowitej, spowodowałoby niepowodzenie dopasowania.

    // pattern = "http://ads.android.com/session/{id}"
    @Serializable data class Session(val id: Int) : NavKey
  • Podobnie, jeśli chcesz mieć opcjonalne argumenty, np. dane analityczne dołączone do adresu URL, mogą to być właściwości klucza, które mają wartości domyślne.

    // pattern = "http://ads.android.com/session/{id}#referrer={referrer}
    @Serializable data class Session(
      val id: Int,
      val referrer: String? = null,
    ) : NavKey
  • Zauważysz, że bardzo staraliśmy się oddzielić strukturę adresu URL, który może należeć do innego zespołu, od klas bezpiecznych pod względem typów lub klas, które reprezentują dopasowanie precyzyjnego linku w aplikacji.

  • Wiem, że niektórzy z Was mają do czynienia z naprawdę okropnymi precyzyjnymi linkami.

    // "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
  • Możesz na przykład mieć obraz zakodowany w formacie base64 dołączony do precyzyjnego linku.

  • Na przykład gigantyczny obiekt JSON, który postanowili umieścić w każdym adresie URL aplikacji.

  • Teraz możesz to zrobić, nawet jeśli nie możesz zmienić podanego adresu URL, ale nadal musisz wyodrębnić z niego pewne informacje.

  • Pierwsza technika polega na rozszerzeniu klasy DeepLinkSerializer, aby dokładnie kontrolować sposób deserializacji lub przekształcania tego argumentu z ciągu znaków na właściwość z zabezpieczeniami wpisywania.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      ...
    }
  • DeepLinkSerializer to pomocniczy kod wokół serializacji Kotlin, który upraszcza implementację klasy serializatora, ponieważ w tym przypadku dane wejściowe deserializatora są zawsze ciągiem znaków z adresu URL.

    public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
  • Aby zaimplementować DeepLinkSerializer, musisz napisać 3 metody.

  • Najpierw wybierz nazwę serii.

    object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
      override val serialName: String
        get() = "Bitmap"
    
      ...
    }
  • Nie musi być w przestrzeni nazw, ale musi być na tyle unikalna, aby serializacja Kotlin wiedziała, kiedy użyć Twojego serializatora.

  • Następnie musisz zaimplementować metodę deserialize, która otrzymuje ciąg znaków z adresu URL i odpowiada za utworzenie obiektu z zabezpieczeniami wpisywania.

    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)
      }
    
       ...
    
    }
  • Na koniec zaimplementujesz serializację, która jest przeciwnym podejściem i jest używana, gdy ta sama klasa klucza jest używana w przypadkach takich jak 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)
      }
    }
  • Teraz zastosujesz niestandardowy serializator do właściwości, która ma tę klasę, która jest nieco wyjątkowa, a web UriDeepLinkMatcher zajmie się resztą.

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

Wstępne przetwarzanie argumentów adresu URL za pomocą niestandardowych dopasowań

  • Co jednak, jeśli potrzebujesz tylko części danych z tego ciągu Base64 lub pliku JSON dołączonego do adresu URL?

  • W takim przypadku warto wstępnie przetworzyć niektóre adresy URL, zanim trafią do serializacji Kotlin.

  • Aby obsłużyć takie przypadki, możesz rozszerzyć klasę UriDeepLinkMatcher i zastąpić jej metodę matchArguments.

  • W przypadku obiektu blob JSON może nam zależeć tylko na identyfikatorze sesji, a reszta danych może być nieistotna.

  • Tworzymy więc podklasę i zastępujemy metodę 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>? {
        ...
    }
  • Dane wejściowe to nieprzetworzone ciągi znaków, czyli wszystkie obiekty zastępcze dodane ze ścieżki identyfikatora URI.

  • Dzięki temu możemy wstępnie przetworzyć i edytować te wartości, zanim wywołamy 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)
    }
  • Teraz możemy mieć tutaj uproszczone wartości i nie musimy wcale zajmować się serializacją w Kotlin.

Tworzenie syntetycznych stosów wstecznych na potrzeby precyzyjnych linków

  • Dziękuję, Ian.

  • Mamy nadzieję, że nie będziesz musiał(a) często korzystać z tych technik, ale poświęciliśmy dużo uwagi temu, jak możesz kontrolować zachowanie dopasowania.

  • OK.

  • Wiemy już, jak uzyskać NavKey za pomocą dopasowań precyzyjnych linków, ale stos wsteczny zawierający tylko klucz nawigacyjny może nie zapewniać najlepszych wrażeń użytkownika.

  • Aby zapewnić prawidłowe działanie przycisku Wstecz, potrzebujesz syntetycznego stosu wstecznego.

  • Część syntetyczna wynika z faktu, że stos wsteczny zawiera klucze, do których użytkownik nigdy nie przeszedł ręcznie, ale jest to ścieżka, którą by przeszedł, gdyby zrobił to ręcznie.

  • Na przykład kliknięcie powiadomienia o sesji, która ma się rozpocząć, powinno przenieść użytkownika bezpośrednio do tej sesji, ale gdy użytkownik uruchomi systemowy przycisk Wstecz, prawdopodobnie chcesz, aby wrócił do ekranu głównego, zanim wyjdzie do programu uruchamiającego.

  • Ten ekran główny staje się częścią syntetycznego stosu wstecznego.

  • W przypadku interfejsów API precyzyjnych linków chcieliśmy zrównoważyć 2 konkurujące ze sobą wymagania.

  • Z jednej strony wiemy, że różne precyzyjne linki lub różne dopasowania mogą wymagać innej logiki do tworzenia syntetycznego stosu wstecznego.

  • Na przykład dodanie HomeScreen przed każdym precyzyjnym linkiem może działać, dopóki nie chcesz utworzyć precyzyjnego linku do ekranu podrzędnego na ekranie ustawień.

  • Teraz potrzebujesz dodatkowej warstwy w syntetycznym stosie wstecznym dla tego konkretnego precyzyjnego linku.

  • Oczywiście wiąże to kreator stosu wstecznego z każdym dopasowaniem precyzyjnego linku.

  • Z drugiej strony wiemy, że każda milisekunda, która upływa, zanim wyświetli się właściwy ekran, kosztuje Cię prawdziwe pieniądze.

  • Nie chcieliśmy więc obliczać syntetycznego stosu wstecznego dla każdego wyniku dopasowania.

  • Chcieliśmy znaleźć najlepsze dopasowanie.

  • Co więc zrobić, jeśli chcemy mieć niestandardową logikę dla każdego dopasowania, ale nie chcemy tworzyć syntetycznego stosu wstecznego dla każdego dopasowania?

  • Po kilku nieudanych próbach znaleźliśmy podejście, które spełnia oba wymagania.

  • Niezależnie od rodzaju dopasowania możesz je rozszerzyć, wywołując withBackStack, aby leniwie utworzyć stos wsteczny z danym MatchResult.

    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)
  • Jeśli więc mamy kilka dopasowań, każde z nich będzie miało własne wywołanie withBackStack, aby utworzyć odpowiedni stos wsteczny dla danego elementu NavKey.

  • Kod będzie najbardziej przejrzysty, jeśli każdy wywoływany obiekt dopasowujący będzie używać metody withBackStack, która zwraca listę obiektów dopasowujących stosu wstecznego.

  • Gdy znajdziesz najlepsze dopasowanie, otrzymasz wynik dopasowania BackStackMatchResult.

  • Jest to podklasa MatchResult, która dodaje nową właściwość o nazwie backStack. Jest to cały stos wsteczny wygenerowany przez funkcję lambda withBackStack.

  • Takie podejście warstwowe pozwoliło nam oddzielić logikę wewnątrz dopasowań od dowolnej logiki niestandardowej, której chcesz używać w funkcji lambda withBackStack.

Szybkie pytania, podsumowanie i zasoby

  • To jest krótka prelekcja, więc niestety kończy nam się czas, ale może uda nam się omówić kilka innych funkcji, które mamy w Navigation 3 i które znajdziesz w naszych przepisach na Navigation 3.

  • OK.

  • Możesz więc dodać filtry do dowolnego dopasowania, aby skrócić logikę dopasowywania i szybko wykluczyć dopasowanie na podstawie działania, typu MIME lub własnych filtrów niestandardowych.

  • Dzieje się to przed analizą identyfikatora URI lub niestandardową serializacją, czyli serializacją Kotlin.

  • Oferujemy też StaticKeyDeepLinkMatcher, który jest przeznaczony do przypadków, gdy nie musisz przeprowadzać analizy, aby uzyskać NavKey, ale chcesz zastosować filtry.

    @Serializable object ImagesKey: NavKey
    
    // custom filter
    val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
    
    val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
  • Jest to idealne rozwiązanie w przypadku precyzyjnych linków, które obsługują tylko działania lub typy MIME, np. aplikacji do edycji zdjęć, która może otwierać obrazy JPEG.

  • Jeśli chodzi o działanie i typ MIME, do obiektu DeepLinkRequest możesz dołączyć dowolny niestandardowy obiekt RequestExtras, który może być używany przez niestandardowe filtry i podklasy DeepLinkRequest w ramach ich logiki.

  • Umożliwia to łączenie platform testów A/B lub sygnałów zewnętrznych.

  • Mamy też całe interfejsy API z zabezpieczeniami wpisywania dla dodatków.

    val extras = requestExtras {
        put(SectionExtrasKey, "A")
    }
  • Prawie cała ta logika znajduje się we wspólnym kodzie KMP Nav3, więc wszystkie dopasowania i logika stosu wstecznego mogą być współdzielone na różnych platformach.

  • Jeśli chcesz dowiedzieć się więcej o tym, jak Navigation 3 obsługuje precyzyjne linki, zapoznaj się z naszymi przepisami dotyczącymi Nav3.

  • Tak, zintegrowaliśmy też nasze interfejsy API precyzyjnych linków z naszymi przepisami DI oraz z naszą główną aplikacją NowInAndroid.

  • Dziękujemy za wysłuchanie naszej krótkiej prelekcji i życzymy udanego korzystania z nawigacji.

  • Dziękujemy.

Kod

Fragmenty kodu

Fragment kodu w języku Kotlin w 04:41 w filmie „Projektowanie interfejsu API precyzyjnych linków i dopasowywanie wzorców identyfikatorów URI”
val matcher = UriDeepLinkMatcher(
  ...,
  ...
)
Fragment kodu w języku Kotlin w 04:53 w filmie „Deep link API design and URI pattern matching”
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri(...),
  ...
)
Fragment kodu w języku Kotlin w 05:04 w filmie „Deep link API design and URI pattern matching”
val matcher = UriDeepLinkMatcher(
  uriPattern = DeepLinkUri("ads.android.com/home"),
  ...
)
Fragment kodu w języku Kotlin w 05:12 w filmie „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}"),
  …
)
Fragment kodu w Kotlinie w 05:39 w filmie „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>()
)
Fragment kodu w języku Kotlin w 05:48 w filmie „Projektowanie interfejsu API precyzyjnych linków i dopasowywanie wzorców identyfikatorów 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>()
)
Fragment kodu w języku Kotlin w 06:12 w filmie „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)
Fragment kodu w języku Kotlin w 07:11 w filmie „Deep link API design and URI pattern matching”
val matchers = listOf(...)

// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
  matcher.match(request)
}
Fragment kodu w Kotlinie w 07:48 w filmie „Projektowanie interfejsu API precyzyjnych linków i dopasowywanie wzorców identyfikatorów URI”
val matchers = listOf(...)

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

// compare matches
val backStack = listOf(
  matchResults.maxOrNull()?.key ?: Home
)
Fragment kodu w języku Kotlin w 08:04 w filmie „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>>
Fragment kodu w języku Kotlin w 09:15 w filmie „Type-safe deserialization and custom serializers”
// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey
Fragment kodu w języku Kotlin w 09:27 w filmie „Type-safe deserialization and custom serializers”
// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
  val id: Int,
  val referrer: String? = null,
) : NavKey
Fragment kodu w języku Kotlin w 09:53 w filmie „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
Fragment kodu w Kotlinie w 10:19 w filmie „Type-safe deserialization and custom serializers”
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  ...
}
Fragment kodu w języku Kotlin w 10:32 w filmie „Type-safe deserialization and custom serializers”
public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>
Fragment kodu w Kotlinie w 10:51 w filmie „Type-safe deserialization and custom serializers”
object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
  override val serialName: String
    get() = "Bitmap"

  ...
}
Fragment kodu w języku Kotlin w 11:01 w filmie „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)
  }

   ...

}
Fragment kodu w języku Kotlin w 11:13 w filmie „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)
  }
}
Fragment kodu w języku Kotlin w 11:24 w filmie „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
Fragment kodu w języku Kotlin w 12:09 w filmie „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>? {
    ...
}
Fragment kodu w języku Kotlin w 12:21 w filmie „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)
}
Fragment kodu w Kotlinie w 14:31 w filmie „Tworzenie syntetycznych stosów wstecznych na potrzeby precyzyjnych linków”
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)
Fragment kodu w języku Kotlin w 15:52 w filmie „Lightning round, summary, and resources”
@Serializable object ImagesKey: NavKey

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

val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))
Fragment kodu w języku Kotlin w 16:24 w filmie „Lightning round, summary, and resources”
val extras = requestExtras {
    put(SectionExtrasKey, "A")
}