Tworzenie elastycznej nawigacji

Nawigacja to interakcja użytkownika z interfejsem aplikacji, która umożliwia dostęp do treści. Zasady nawigacji w Androidzie zawierają wytyczne, które pomogą Ci tworzyć spójną i intuicyjną nawigację w aplikacji.

Elastyczne/adaptacyjne interfejsy użytkownika zapewniają elastyczne miejsca docelowe treści i często zawierają różne typy elementów nawigacji w odpowiedzi na zmiany rozmiaru ekranu – np. pasek nawigacyjny na dole na małych ekranach, szynę nawigacyjną na ekranach średniej wielkości lub stały drawer nawigacyjny na dużych ekranach – ale interfejsy elastyczne/adaptacyjne powinny nadal być zgodne z zasadami nawigacji.

Komponent nawigacji w Jetpacku wdraża zasady nawigacji i ułatwia tworzenie aplikacji z responsywnym lub dostosowującym się interfejsem użytkownika.

Rysunek 1. Rozwinięte, średnie i kompaktowe wyświetlacze z drawerem nawigacji, szyną i dolnym paskiem.

Responsive UI navigation

Rozmiar okna wyświetlania zajmowanego przez aplikację wpływa na ergonomię i użyteczność. Klasy rozmiarów okna umożliwiają określenie odpowiednich elementów nawigacji (takich jak paski nawigacyjne, szyny lub szuflady) i umieszczenie ich w miejscu, w którym są najbardziej dostępne dla użytkownika. Zgodnie z wytycznymi dotyczącymi układu w Material Design elementy nawigacji zajmują stały obszar na przedniej krawędzi ekranu i mogą przesuwać się na dolną krawędź, gdy aplikacja ma kompaktową szerokość. Wybór elementów nawigacji zależy głównie od rozmiaru okna aplikacji i liczby elementów, które element musi zawierać.

Klasa rozmiaru okna Niewiele elementów Wiele elementów
Szerokość kompaktowa dolny pasek nawigacyjny panel nawigacji (górna krawędź lub dół)
średnia szerokość kolumna nawigacji panel nawigacji (przednia krawędź)
szerokość po rozwinięciu kolumna nawigacji stały panel nawigacji (przednia krawędź)

Pliki zasobów układu można klasyfikować według punktów przecięcia klasy rozmiaru okna, aby używać różnych elementów nawigacji w różnych wymiarach wyświetlania.

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

Miejsca docelowe z dopasowaną zawartością

W interfejsie elastycznym układ każdej treści docelowej dostosowuje się do zmian rozmiaru okna. Aplikacja może dostosowywać odstępy w układzie, zmieniać położenie elementów, dodawać lub usuwać treści oraz zmieniać elementy interfejsu, w tym elementy nawigacyjne.

Gdy każde poszczególne miejsce docelowe obsługuje zdarzenia zmiany rozmiaru, zmiany są izolowane w interfejsie. Nie ma to wpływu na pozostałe stany aplikacji, w tym nawigację.

Nawigacja nie powinna być efektem ubocznym zmiany rozmiaru okna. Nie twórz miejsc docelowych treści tylko po to, aby dostosować je do różnych rozmiarów okna. Nie twórz na przykład różnych miejsc docelowych treści dla różnych ekranów składanego urządzenia.

Przechodzenie do miejsc docelowych treści w efekcie ubocznym zmian rozmiaru okna powoduje te problemy:

  • Przed przejściem do nowego miejsca docelowego przez chwilę może być widoczne stare miejsce docelowe (dla poprzedniego rozmiaru okna).
  • Aby zachować odwracalność (np. gdy urządzenie jest złożone i rozłożone), nawigacja jest wymagana dla każdego rozmiaru okna.
  • Utrzymanie stanu aplikacji między miejscami docelowymi może być trudne, ponieważ nawigacja może zniszczyć stan po wygenerowaniu stosu wstecznego.

Podczas zmiany rozmiaru okna aplikacja może nie być widoczna na pierwszym planie. Układ Twojej aplikacji może wymagać więcej miejsca niż aplikacja na pierwszym planie, a gdy użytkownik wróci do Twojej aplikacji, orientacja i rozmiar okna mogą się zmienić.

Jeśli Twoja aplikacja wymaga unikalnych miejsc docelowych z treściami na podstawie rozmiaru okna, rozważ połączenie odpowiednich miejsc docelowych w jedno miejsce docelowe, które zawiera alternatywne, elastyczne układy.

Miejsca docelowe z układami alternatywnymi

W ramach elastycznego lub adaptacyjnego projektu pojedynczy element nawigacji może mieć alternatywne układy w zależności od rozmiaru okna aplikacji. Każdy układ zajmuje całe okno, ale w przypadku różnych rozmiarów okien wyświetlane są różne układy (projekt adaptacyjny).

Przykładem kanonicznego widoku jest widok listy i szczegółów. W przypadku kompaktowych rozmiarów okna aplikacja wyświetla jeden układ treści na liście i jeden w oknie z informacjami. Po przejściu do miejsca docelowego widoku szczegółów listy początkowo wyświetla się tylko układ listy. Po wybraniu pozycji na liście aplikacja wyświetla układ szczegółów, zastępując listę. Gdy wybierzesz przycisk Wstecz, wyświetli się układ listy, który zastąpi szczegóły. Jednak w przypadku powiększonego okna układy listy i szczegółów są wyświetlane obok siebie.

SlidingPaneLayout umożliwia tworzenie pojedynczego miejsca docelowego nawigacji, które na dużych ekranach wyświetla 2 panele treści obok siebie, a na małych ekranach, np. na zwykłych telefonach, tylko 1 panel naraz.

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

Szczegółowe informacje o wdrażaniu układu listy z poziomymi szczegółami za pomocą SlidingPaneLayout znajdziesz w artykule Tworzenie układu z 2 panelami.

Jeden wykres nawigacyjny

Aby zapewnić użytkownikom spójne wrażenia na dowolnym urządzeniu lub przy dowolnym rozmiarze okna, użyj pojedynczego grafu nawigacji, w którym układ każdej strony docelowej jest responsywny.

Jeśli w przypadku każdej klasy rozmiaru okna używasz innego diagramu nawigacji, gdy aplikacja przechodzi z jednej klasy rozmiaru do drugiej, musisz określić bieżący cel użytkownika w innych diagramach, utworzyć stos odwrotny i zgodnie z tym zweryfikować informacje o stanie, które różnią się między diagramami.

Zagnieżdżony host nawigacji

Twoja aplikacja może zawierać miejsce docelowe z miejscami docelowymi. Na przykład w układzie listy z szczegółami panel szczegółów produktu może zawierać elementy interfejsu, które umożliwiają przejście do treści zastępujących szczegóły produktu.

Aby wdrożyć ten typ nawigacji podrzędnej, ustaw panel szczegółowy jako hosta nawigacji zagnieżdżonej z własnym grafem nawigacji, który określa miejsca docelowe dostępne z panelu szczegółowego:

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

Różni się to od zagnieżdżonego wykresu nawigacji, ponieważ wykres nawigacji zagnieżdżonego NavHost nie jest połączony z głównym wykresem nawigacji. Oznacza to, że nie można przejść bezpośrednio z miejsc docelowych na jednym wykresie do miejsc docelowych na drugim.

Więcej informacji znajdziesz w artykule Głębokie diagramy nawigacji.

Zachowany stan

Aby zapewnić strony docelowe z dopasowującymi się do urządzenia treściami, aplikacja musi zachować swój stan, gdy urządzenie zostanie obrócone lub złożone albo gdy zmieni się rozmiar okna aplikacji. Domyślnie takie zmiany konfiguracji jak te ponownie tworzą czynności, fragmenty i hierarchię widoku aplikacji. Zalecany sposób zapisywania stanu interfejsu to użycie atrybutu ViewModel, który jest zachowywany po zmianach konfiguracji. (zobacz stany ui „Zapisz” ).

Zmiany rozmiaru powinny być odwracalne, np. gdy użytkownik obróci urządzenie, a potem obróci je z powrotem.

Układy elastyczne/adaptacyjne mogą wyświetlać różne treści w różnych rozmiarach okna. Dlatego często muszą one zapisywać dodatkowy stan związany z treścią, nawet jeśli nie jest on odpowiedni dla bieżącego rozmiaru okna. Na przykład układ może mieć miejsce na dodatkowy element przewijania tylko przy większej szerokości okna. Jeśli zdarzenie zmiany rozmiaru spowoduje, że szerokość okna będzie zbyt mała, widżet zostanie ukryty. Gdy aplikacja zmieni rozmiar na poprzedni, widżet przewijania znów będzie widoczny, a początkowa pozycja przewijania powinna zostać przywrócona.

Zakresy ViewModel

W przewodniku dla deweloperów Przejście na komponent Nawigacja opisano architekturę z jedną aktywnością, w której miejsca docelowe są implementowane jako fragmenty, a ich modele danych są implementowane za pomocą ViewModel.

ViewModel jest zawsze ograniczone do cyklu życia, a gdy ten cykl zakończy się na stałe, ViewModel zostaje wyczerpane i można je odrzucić. Cykl życia, w którym jest używany ViewModel (a zatem zakres, w jakim można udostępniać ViewModel), zależy od obiektu zastępczego używanego do uzyskiwania wartości ViewModel.

W najprostszym przypadku każde miejsce docelowe nawigacji to pojedynczy fragment z całkowicie odizolowanym stanem UI. Oznacza to, że każdy fragment może używać obiektu zastępczego viewModels(), aby uzyskać obiekt ViewModel ograniczony do tego fragmentu.

Aby udostępniać stan UI między fragmentami, ogranicz ViewModel do aktywności, wywołując activityViewModels() w fragmentach (odpowiednikiem Activity jest tylko viewModels()). Dzięki temu aktywność i wszystkie fragmenty, które do niej dołączają, mogą udostępniać instancję ViewModel. Jednak w architekturze z pojedynczą aktywnością zakres ViewModel trwa praktycznie tak długo, jak aplikacja, więc ViewModel pozostaje w pamięci, nawet jeśli żadne fragmenty z niego nie korzystają.

Załóżmy, że graf nawigacji zawiera sekwencję miejsc docelowych fragmentów reprezentujących proces płatności, a bieżący stan całego procesu płatności znajduje się w elementach ViewModel, które są wspólne dla fragmentów. Zakresowanie ViewModel do aktywności jest nie tylko zbyt szerokie, ale też powoduje inny problem: jeśli użytkownik przejdzie przez proces płatności w przypadku jednego zamówienia, a potem ponownie w przypadku drugiego, oba zamówienia będą używać tej samej instancji płatności ViewModel. Przed opłaceniem drugiego zamówienia musisz ręcznie wyczyścić dane z pierwszego zamówienia. Wszelkie błędy mogą być kosztowne dla użytkownika.

Zamiast tego ogranicz ViewModel do grafu nawigacyjnego w bieżącym pliku NavController. Utwórz zagnieżdżony graf nawigacji, aby zawierać miejsca docelowe, które są częścią procesu płatności. Następnie w przypadku każdego z tych fragmentów docelowych użyj obiektu zastępczego navGraphViewModels() i przekaż identyfikator ViewModel grafu nawigacyjnego, aby uzyskać udostępniony ViewModel. Dzięki temu, gdy użytkownik opuści proces płatności, a odpowiednia instancja ViewModel zostanie usunięta i nie będzie używana podczas następnego procesu płatności.

Zakres Przedstawiciel usługi Możesz udostępnić ViewModel użytkownikom
Fragment Fragment.viewModels() Tylko fragment
Aktywność Activity.viewModels() lub Fragment.activityViewModels() Aktywność i wszystkie dołączone do niej fragmenty
Graficzny widok nawigacji Fragment.navGraphViewModels() Wszystkie fragmenty w tym samym grafie nawigacji

Pamiętaj, że jeśli używasz hosta z zagnieżdżoną nawigacją (patrz sekcja Host z zagnieżdżoną nawigacją), miejsca docelowe w tym hostie nie mogą udostępniać instancji ViewModel miejscom docelowym spoza tego hosta, gdy używasz navGraphViewModels(), ponieważ te grafy nie są połączone. W takim przypadku możesz użyć zakresu aktywności.

Dodatkowe materiały