Inżynier oprogramowania
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.
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.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.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.Transkrypcja
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.
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.
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) : NavKeyPodobnie, 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,
) : NavKeyZauważ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,
) : NavKeyMoż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
) : NavKeyCo 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.
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.
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.
val matcher = UriDeepLinkMatcher(
...,
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri(...),
...
)val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/home"),
...
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
…
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// target NavKey
@Serializable data class Session(val id: String) : NavKey
val matcher = UriDeepLinkMatcher(
uriPattern = DeepLinkUri("http://ads.android.com/session/{id}"),
serializer = serializer<Session>()
)// 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)val matchers = listOf(...)
// gets all non-null MatchResult
val matchResults = matchers.mapNotNull { matcher ->
matcher.match(request)
}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
)// 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>>// pattern = "http://ads.android.com/session/{id}"
@Serializable data class Session(val id: Int) : NavKey// pattern = "http://ads.android.com/session/{id}#referrer={referrer}
@Serializable data class Session(
val id: Int,
val referrer: String? = null,
) : NavKey// "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,
) : NavKeyobject BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}public abstract class DeepLinkSerializer<T : Any> : KSerializer<T>object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
override val serialName: String
get() = "Bitmap"
...
}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)
}
...
}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)
}
}object BitmapSerializer : DeepLinkSerializer<Bitmap>() {
...
}
@Serializable data class Session(
val id: String,
@Serializable(BitmapSerializer::class)
val placeholderImage: Bitmap? = null
) : NavKeyclass 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>? {
...
}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)
}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)@Serializable object ImagesKey: NavKey
// custom filter
val mimeTypeFilter = DeepLinkMatcher.mimeTypeFilter("image/jpg")
val matcher = StaticKeyDeepLinkMatcher(ImagesKey, listOf(mimeTypeFilter))val extras = requestExtras {
put(SectionExtrasKey, "A")
}