Jetpack Compose jest nowoczesną biblioteką do tworzenia natywnych aplikacji na platformę Android. W znacznym stopniu usprawnia ona proces tworzenia interfejsów użytkownika (UI), umożliwiając przygotowywanie ekranów i komponentów przy wykorzystaniu deklaratywnego kodu napisanego w języku Kotlin. Compose oferuje również własny zestaw rozwiązań do zarządzania nawigacją w aplikacji.
W artykule przyjrzymy się temu, jak funkcjonuje nawigacja w Jetpack Compose, jak ją poprawnie zaimplementować oraz w jaki sposób przekazywać złożone struktury pomiędzy ekranami.
Implementacja w projekcie
Aby zaimplementować w projekcie bibliotekę navigation-compose, musimy dodać odpowiednią zależność do pliku build.gradle:

Dokładny sposób implementacji może się różnić zależnie od zastosowanego sposobu konfiguracji projektu.
Komponenty nawigacyjne Jetpack Compose
Do zarządzania nawigacją przy z użyciem biblioteki navigation-compose wykorzystuje się następujące komponenty:
| Komponent | Charakterystyka |
| NavHost | Komponent definiujący miejsce, w którym odbywa się nawigacja. Wyświetla aktualną destynację aplikacji. |
| NavController | Centralny komponent zarządzający nawigacją pomiędzy destynacjami. Dysponuje metodami pozwalający m.in na nawigowanie, obsługę deep linków i cofanie po destynacjach. |
| NavGraph | Struktura danych definiująca wszystkie destynacje w aplikacji oraz określająca jak są ze sobą powiązane. |
| Destination | Węzeł w grafie nawigacyjnym, określające „miejsce” w aplikacji, do którego chcemy nawigować. W większości przypadków będzie to ekran composable’owy, chociaż biblioteka umożliwia również stosowanie jako destynacji dialogów oraz zagnieżdżonych grafów nawigacyjnych. |
| Route | Komponent, który w sposób unikatowy definiuje destynację (cel) nawigacji oraz wszelkie dane przez nią wymagane. |
Poniżej przedstawiono przykład podstawowej struktury nawigacji:

W powyższym przykładzie:
- Utworzony obiekt navController zarządza nawigacją.
- NavHost inicjuje strukturę nawigacji. Przekazane mu argumenty to utworzony wcześniej navController oraz startDestination, wskazujące na ekran początkowy aplikacji (w tym przypadku screenA).
- W ciele NavHosta zdefiniowano NavigationGraph, wskazując poszczególne destynacje aplikacji. W tym przypadku występują dwie destynacje composable: screenA oraz screenB.
- Destynacje composable zawierają w sobie określone ekrany w formie funkcji composable.
Przekazywanie danych w trakcie nawigacji – ścieżki z argumentami
Częstym scenariuszem w przypadku przygotowywania aplikacji mobilnej jest konieczność przekazywania danych pomiędzy różnymi ekranami. Jednym ze sposobów realizacji tego zadania przy użyciu Jetpack Compose Navigation jest skorzystanie wykorzystanie ścieżek z argumentami.
W celu przedstawienia tego konceptu zaplanowano scenariusz, w którym z destynacji screenA należy przekazać do destynacji screenB takie dane jak userId oraz userName.
Poniżej przedstawiono przykład implementacyjny, będący rozszerzeniem wcześniej przygotowanego kodu:

W powyższym przykładzie:
- route destynacji screenB został rozszerzony o 2 argumenty: userId oraz userName.
- Argumenty userId oraz userName zostały wylistowane w polu arguments. Poza określeniem nazw poszczególnych argumentów wskazywany jest również ich typ – odpowiednio NavType.IntType dla userId oraz NavType.StringType dla userName.
- W ciele destynacji composable następuje próba pobrania określonych wcześniej argumentów z NavBackStackEntry poprzez podanie ich kluczy (userId oraz userName).
- Pobrane dane są następnie przekazywane do ekranu ScreenB.
Wywołanie funkcji do nawigacji na navControllerze przy wykorzystaniu ścieżek z argumentami jest widoczne na poniższym przykładzie prezentującym @Composable ScreenA:

W powyższym przykładzie:
- Zadeklarowano argumenty userId oraz userName wartościami 123 oraz Jacek.
- Nawigacja do destynacji screenB następuje poprzez wywołanie całej ścieżki z argumentami: „screenB/${userId}/${userName}”.
Nawigacja przy wykorzystaniu ścieżek z argumentami jest prosta w implementacji i łatwo rozszerzalna. Potencjalne problemy mogą być spowodowane tym, iż ten sposób nie zapewnia type safety. Typ każdegoz argumentów jest ustalany ręcznie przez dewelopera, a ich poprawność nie jest sprawdzana w trakcie kompilacji aplikacji. Występowanie błędów podczas runtime’u wydłuża proces developmentu. Dodatkowo podejście ścieżek z argumentami wymusza pisanie dużych ilości kodu boilerplate w celu określenia dokładnych adresów destynacji oraz typów poszczególnych argumentów.
W przygotowanym przykładzie do adresów wykorzystano route’y w postaci czystych obiektów String. W zastosowaniach komercyjnych korzysta się z tego sposobu, zabezpieczając stringowe route’y np. poprzez utworzenie sealed class i zapis adresów w ich polach.
Przekazywanie danych w trakcie nawigacji – obiekty typesafe @Serializable
Google w swojej oficjalnej dokumentacji proponuje korzystanie z innej metody nawigacji w nowych projektach. Polega ona na wykorzystaniu obiektów i/lub klas oznaczonych jako @Serializable, które zastępują omówione wcześniej stringowe route’y.
Aby móc skorzystać z tagowania @Serializable do nawigacji, konieczne jest dodanie odpowiedniej zależności do pliku build.gradle:


Po synchronizacji możliwe będzie skorzystanie z tagów @Serializable w celu przygotowania unikalnych obiektów i klas, które posłużą w celach nawigacyjnych.
Przykład implementacyjny przygotowałem poniżej.

W powyższym przykładzie:
- Zadeklarowano object ScreenA pełniący funkcję identyfikatora ekranu @Composable ScreenA. Nawigacja do ekranu ScreenA nie wymaga przekazania argumentów, dlatego wykorzystanie struktury object jest wystarczające.
- Zadeklarowano data class ScreenB będący identyfikatorem ekranu @Composable ScreenB. Jak we wcześniej omówionym przykładzie, ScreenB oczekuje przekazania mu dwóch argumentów: userId oraz userName. Z tego powodu indentyfikatorem jest data class z dwoma polami. Pole userName jest opcjonalne, dlatego zostało oznaczone jako nullable z domyślną wartością.
Po przygotowaniu identyfikatorów ekranów w postaci klas i obiektów @Serializable możliwe jest utworzenie nowego grafu nawigacyjnego. Wykorzystanie tego sposobu znacznie upraszcza strukturę grafu, co jest widoczne na załączonym przykładzie:

W powyższym przykładzie:
- navController został utworzony w taki sam sposób, jak przy nawigacji z route’ami.
- Jako startDestination wskazano @Serializable object ScreenA.
- Destynacje composable mają bezpośrednio przekazany identyfikator ScreenA / ScreenB.
- Dostęp do przekazanych argumentów odbywa się poprzez wywołanie metody toRoute<>() z określeniem typu (w tym przypadku ScreenB).
Wywołanie funkcji do nawigacji na navControllerze przy wykorzystaniu ścieżek z argumentami jest widoczny na poniższym przykładzie prezentującym odpowiednio zmodyfikowany @Composable ScreenA:

W powyższym przykładzie:
- Nawigacja do ekranu ScreenB odbywa się poprzez wywołanie metody navigate() na navController, z przekazaniem nowo utworzonego obiektu klasy ScreenB z odpowiednio określonymi parametrami userId oraz userName.
Korzyści z wykorzystania obiektów i klas otagowanych jako @Serializable
Nawigacja przy wykorzystaniu obiektów i klas otagowanych jako @Serializable niesie za sobą wiele korzyści.
Przede wszystkim jest to rozwiązanie type safe. Deweloper nie musi się obawiać popełnienia pomyłki przy określaniu typu argumentu w sytuacji, jeżeli jest niezbędne przekazanie danych do kolejnego ekranu. Argumenty, jako pola data class, mają z góry określony typ, zatem w przypadku pomyłki niemożliwe będzie skompilowanie aplikacji. Dodatkowo, opisane rozwiązanie jest zdecydowanie bardziej czytelne dzięki braku konieczności pisania kodu boilerplate określającego typ i nazwę każdego z argumentów nawigacyjnych.
Type safe navigation jest domyślnym rozwiązaniem wskazywanym w dokumentacji Android do nawigacji pomiędzy ekranami jak i do przekazywania danych pomiędzy nimi. Należy jednocześnie pamiętać o tym, że nie zaleca się przekazywania skomplikowanych struktur w trakcie nawigacji. Dobrą praktyką jest przekazywanie maksymalnie prostych informacji, jak np. klucz ID, a odczyt pozostałych danych powinien nastąpić z oddzielnego źródła, np. repozytorium. Dzięki pojedynczemu źródłu prawdy będziemy mieli pewność, że wymagane dane w naszej destynacji są aktualne.
Nawigacja wstecz
W Jetpack Compose nawigacja wstecz jest obsługiwana za pomocą funkcji popBackStack(), wywoływanej
na navControllerze. Umożliwia ona cofnięcie się do poprzedniego ekranu poprzez usunięcie aktualnego ekranu ze stosu – w efekcie następuje powrót do poprzedniego widoku.
W celu wykonania nawigacji wstecz do konkretnego ekranu znajdującego się na stosie nawigacyjnym możliwe jest wywołanie jednej z metod będącej przeciążeniem popBackStack(). Identyfikator docelowej destynacji można przekazać w formie route będącej obiektem typu String lub przy wykorzystaniu obiektów i klas @Serialized, co zaprezentowano na poniższym przykładzie:

W powyższym przykładzie:
- Na navController następuje wywołanie przeciążonej metody popBackStack() z przekazaniem typu naszej destynacji @Serialized ScreenA.
- Parametr inclusive w metodzie popBackStack() określa, czy ze stosu nawigacyjnego ma również zostać usunięta destynacja, do której następuje nawigacja wstecz.
Nawigacja wstecz z przekazywaniem argumentu
Biblioteka nawigacyjna zapewnia narzędzia, które umożliwiają nawigowanie wstecz do poprzednich ekranów z przekazaniem informacji zwrotnej. Przesłanie informacji obywa się poprzez aktualizację previousBackStackEntry i savedStateHandle oraz odczyt przekazywanych danych na poziomie grafu nawigacyjnego.
Poniżej znajdziecie wcześniejszy przykład zmodyfikowany o możliwość przesłania informacji podczas nawigacji wstecz:

W powyższym przykładzie:
- navController umożliwia dostęp do previousBackStackEntry, skąd możliwe jest wywołanie savedStateHandle.
- W savedStateHandle możliwe jest ustawienie argumentu w postaci klucz-wartość (key-value), w tym przypadku kluczem jest numer, a wartość to 456.
- Następnie następuje faktyczna nawigacja poprzez wywołanie metody popBackStack() na navControllerze.
W celu pozyskania danych przekazywanych wstecz konieczna była aktualizacja grafu nawigacyjnego, co przedstawiono poniżej:

W powyższym przykładzie:
- W composable<ScreenA> poprzez odwołanie do savedStateHandle i odniesienie się do klucza number tworzony jest obiekt number – mający wartość przekazaną z ekranu ScreenB (456).
- Pozyskana informacja w postaci obiektu number jest przekazywana do ekranu ScreenA.
Podsumowanie
Nawigowanie między ekranami to nieodłączny element niemal każdej aplikacji mobilnej. Poznanie i zrozumienie narzędzi, które to umożliwiają, jest kluczowe do tworzenia nowoczesnych aplikacji na platformę Android szybko i efektywnie.
Nawigacja w Jetpack Compose jak i w samym systemie Android to bardzo obszerne zagadnienia, których sam opis techniczny w dokumentacji jest zawarty w kilkudziesięciu stronach. W artykule przedstawiłem jedynie podstawowe koncepty i przykłady dotyczące implementacji nawigacji w Jetpack Compose. Stanowią one jednak solidną podstawę do dalszego poznawania technologii, próbowania rozwiązań i wykorzystywania jej w praktyce.
***
Jeśli interesuje Cię obszar mobile, zajrzyj również do innych artykułów naszych specjalistów.
Zostaw komentarz