Creare una navigazione adattabile

La navigazione è l'interazione dell'utente con l'interfaccia utente di un'applicazione per accedere alle destinazioni dei contenuti. I principi di navigazione di Android forniscono linee guida che ti aiutano a creare una navigazione coerente e intuitiva dell'app.

Le UI adattabili/responsive forniscono destinazioni di contenuti adattabili e spesso includono diversi tipi di elementi di navigazione in risposta alle variazioni delle dimensioni dello schermo, ad esempio una barra di navigazione in basso su schermi piccoli, una barra di navigazione su schermi di medie dimensioni o un riquadro di navigazione persistente su schermi di grandi dimensioni, ma devono comunque essere conformi ai principi di navigazione.

Il componente Navigation di Jetpack implementa i principi di navigazione e facilita lo sviluppo di app con UI responsive/adattative.

Figura 1. Display espansi, medi e compatti con riquadro di navigazione, barra laterale e barra in basso.

Navigazione dell'interfaccia utente adattabile

Le dimensioni della finestra di visualizzazione occupata da un'app influiscono sull'ergonomia e sull'usabilità. Le classi di dimensioni della finestra ti consentono di determinare gli elementi di navigazione appropriati (come barre di navigazione, riquadri o riquadri a scomparsa) e di posizionarli dove sono più accessibili per l'utente. Nelle linee guida per il layout di Material Design, gli elementi di navigazione occupano uno spazio permanente sul bordo anteriore del display e possono spostarsi sul bordo inferiore quando la larghezza dell'app è compatta. La scelta degli elementi di navigazione dipende in gran parte dalle dimensioni della finestra dell'app e dal numero di elementi che l'elemento deve contenere.

Classe delle dimensioni della finestra Pochi elementi Molti articoli
larghezza compatta barra di navigazione in basso riquadro di navigazione (bordo anteriore o inferiore)
larghezza media barra di navigazione riquadro di navigazione (bordo anteriore)
larghezza espansa barra di navigazione riquadro di navigazione a scomparsa permanente (bordo anteriore)

I file di risorse di layout possono essere qualificati in base alle interruzioni della classe delle dimensioni della finestra per utilizzare elementi di navigazione diversi per dimensioni di visualizzazione diverse.

<!-- 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>

Destinazioni dei contenuti adattabili

In un'interfaccia utente adattabile, il layout di ogni destinazione dei contenuti si adatta alle modifiche delle dimensioni della finestra. L'app può regolare la spaziatura del layout, riposizionare gli elementi, aggiungere o rimuovere contenuti o modificare gli elementi dell'interfaccia utente, inclusi gli elementi di navigazione.

Quando ogni singola destinazione gestisce gli eventi di ridimensionamento, le modifiche vengono isolate per l'interfaccia utente. Il resto dello stato dell'app, inclusa la navigazione, non è interessato.

La navigazione non deve verificarsi come effetto collaterale delle modifiche delle dimensioni della finestra. Non creare destinazioni dei contenuti solo per adattarsi a dimensioni diverse delle finestre. Ad esempio, non creare destinazioni dei contenuti diverse per le diverse schermate di un dispositivo pieghevole.

La navigazione alle destinazioni dei contenuti come effetto collaterale delle modifiche delle dimensioni della finestra presenta i seguenti problemi:

  • La destinazione precedente (per le dimensioni della finestra precedenti) potrebbe essere visibile per un breve periodo prima di passare alla nuova destinazione
  • Per mantenere la reversibilità (ad esempio, quando un dispositivo è chiuso e aperto), la navigazione è obbligatoria per ogni dimensione della finestra
  • Mantenere lo stato dell'applicazione tra le destinazioni può essere difficile, poiché la navigazione può distruggere lo stato al momento del popping della backstack

Inoltre, l'app potrebbe non essere nemmeno in primo piano quando si verificano le modifiche delle dimensioni della finestra. Il layout della tua app potrebbe richiedere più spazio dell'app in primo piano e, quando l'utente torna alla tua app, l'orientamento e le dimensioni della finestra potrebbero essere cambiati.

Se la tua app richiede destinazioni dei contenuti uniche in base alle dimensioni della finestra, valuta la possibilità di combinare le destinazioni pertinenti in un'unica destinazione che includa layout alternativi e adattabili.

Destinazioni dei contenuti con layout alternativi

Nell'ambito di un design responsive/adattabile, una singola destinazione di navigazione può avere layout alternativi a seconda delle dimensioni della finestra dell'app. Ogni layout occupa l'intera finestra, ma vengono presentati layout diversi per dimensioni diverse della finestra (design adattabile).

Un esempio canonico è la visualizzazione elenco-dettagli. Per le dimensioni compatte delle finestre, l'app mostra un layout dei contenuti per l'elenco e uno per i dettagli. Se si accede alla destinazione della visualizzazione dei dettagli dell'elenco, inizialmente viene visualizzato solo il layout dell'elenco. Quando viene selezionato un elemento dell'elenco, l'app mostra il layout dei dettagli, sostituendo l'elenco. Quando il controllo Indietro è selezionato, viene visualizzato il layout dell'elenco, che sostituisce i dettagli. Tuttavia, per le dimensioni della finestra espanse, i layout di elenco e dettaglio vengono visualizzati affiancati.

SlidingPaneLayout ti consente di creare una singola destinazione di navigazione che mostra due riquadri di contenuti affiancati su schermi di grandi dimensioni, ma solo un riquadro alla volta su schermi piccoli, come quelli dei telefoni convenzionali.

<!-- 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>

Per informazioni dettagliate sull'implementazione di un layout di elenco e dettagli utilizzando SlidingPaneLayout, consulta Creare un layout a due riquadri.

Un grafico di navigazione

Per offrire un'esperienza utente coerente su qualsiasi dispositivo o dimensione della finestra, utilizza un singolo grafico di navigazione in cui il layout di ogni destinazione dei contenuti sia adattabile.

Se utilizzi un grafo di navigazione diverso per ogni classe di dimensioni della finestra, ogni volta che l'app passa da una classe all'altra, devi determinare la destinazione corrente dell'utente negli altri grafici, creare una pila di ritorno e riconciliare le informazioni sullo stato che differiscono tra i grafici.

Host di navigazione nidificato

La tua app potrebbe includere una destinazione dei contenuti che ha destinazioni dei contenuti proprie. Ad esempio, in un layout dettagliato dell'elenco, il riquadro dei dettagli dell'articolo potrebbe includere elementi dell'interfaccia utente che rimandano a contenuti che sostituiscono i dettagli dell'articolo.

Per implementare questo tipo di navigazione secondaria, imposta il riquadro dei dettagli come un orientamento di navigazione nidificato con un proprio grafico di navigazione che specifica le destinazioni a cui si accede dal riquadro dei dettagli:

<!-- 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>

È diverso da un grafico di navigazione nidificato perché il grafico di navigazione del NavHost nidificato non è collegato al grafico di navigazione principale, ovvero non puoi passare direttamente dalle destinazioni di un grafico alle destinazioni dell'altro.

Per ulteriori informazioni, consulta la sezione Grafici di navigazione nidificati.

Stato conservato

Per fornire destinazioni di contenuti adattabili, l'app deve mantenere il proprio stato quando il dispositivo viene ruotato o chiuso o quando le dimensioni della finestra dell'app vengono modificate. Per impostazione predefinita, modifiche alla configurazione come queste ricreano le attività, i frammenti e la gerarchia delle visualizzazioni dell'app. Il modo consigliato per salvare lo stato dell'interfaccia utente è con un ViewModel, che rimane invariato anche dopo le modifiche alla configurazione. (vedi Salvare gli stati della UI ).

Le modifiche alle dimensioni devono essere reversibili, ad esempio quando l'utente ruota il dispositivo e poi lo ruota di nuovo.

I layout adattabili possono mostrare contenuti diversi a seconda delle dimensioni della finestra. Pertanto, spesso devono salvare uno stato aggiuntivo relativo ai contenuti, anche se non è applicabile alle dimensioni correnti della finestra. Ad esempio, un layout potrebbe avere spazio per mostrare un widget di scorrimento aggiuntivo solo con larghezze della finestra maggiori. Se un evento di ridimensionamento fa sì che la larghezza della finestra diventi troppo ridotta, il widget viene nascosto. Quando l'app viene ridimensionata alle dimensioni precedenti, il widget scorrevole diventa di nuovo visibile e la posizione di scorrimento originale dovrebbe essere ripristinata.

Ambiti ViewModel

La guida per gli sviluppatori Eseguire la migrazione al componente di navigazione prescrive un'architettura a singola attività in cui le destinazioni vengono implementate come frammenti e i relativi modelli di dati vengono implementati utilizzando ViewModel.

Un ViewModel è sempre associato a un ciclo di vita e, quando questo termina definitivamente, il ViewModel viene eliminato e può essere ignorato. Il ciclo di vita a cui è associato ViewModel e, di conseguenza, la misura in cui ViewModel può essere condiviso, dipendono dal delegato della proprietà utilizzato per ottenere ViewModel.

Nel caso più semplice, ogni destinazione di navigazione è un singolo frammento con uno stato dell'interfaccia utente completamente isolato. Pertanto, ogni frammento può utilizzare il delegato della proprietà viewModels() per ottenere un ViewModel limitato a quel frammento.

Per condividere lo stato dell'interfaccia utente tra i frammenti, definisci l'ambito di ViewModel per l'attività chiamando activityViewModels() nei frammenti (l'equivalente di Activity è semplicemente viewModels()). In questo modo, l'attività e gli eventuali frammenti collegati possono condividere l'istanza ViewModel. Tuttavia, in un'architettura a singola attività, questo ambito ViewModel dura effettivamente per tutta la durata dell'app, quindi ViewModel rimane in memoria anche se nessun fragment lo utilizza.

Supponiamo che il grafo di navigazione abbia una sequenza di destinazioni dei frammenti che rappresentano un flusso di pagamento e che lo stato corrente dell'intera esperienza di pagamento si trovi in un ViewModel condiviso tra i frammenti. L'ambito diViewModel per l'attività non è solo troppo ampio, ma espone anche a un altro problema: se l'utente completa il flusso di pagamento per un ordine e poi lo ripete per un secondo ordine, entrambi gli ordini utilizzano la stessa istanza delViewModel di pagamento. Prima del pagamento del secondo ordine, devi cancellare manualmente i dati del primo ordine. Eventuali errori potrebbero essere costosi per l'utente.

Scegli invece come ambito ViewModel un grafo di navigazione nell'NavController corrente. Crea un grafo di navigazione nidificato per incapsulare le destinazioni che fanno parte del flusso di pagamento. Poi, in ciascuna di queste destinazioni del frammento, utilizza il delegato della proprietà navGraphViewModels() e passa l'ID del grafico di navigazione per ottenere il ViewModel condiviso. In questo modo, quando l'utente esce dal flusso di pagamento e il grafo di navigazione nidificato non è più in ambito, l'istanza corrispondente di ViewModel viene ignorata e non verrà utilizzata per il pagamento successivo.

Ambito Rappresentante della proprietà Può condividere ViewModel con
Frammento Fragment.viewModels() Solo frammento
Attività Activity.viewModels() o Fragment.activityViewModels() Attività e tutti i frammenti associati
Grafico di navigazione Fragment.navGraphViewModels() Tutti i frammenti nello stesso grafo di navigazione

Tieni presente che se utilizzi un host di navigazione nidificato (vedi la sezione Host di navigazione nidificato), le destinazioni in questo host non possono condividere istanze ViewModel con destinazioni esterne all'host quando utilizzi navGraphViewModels() perché i grafici non sono collegati. In questo caso, puoi utilizzare l'ambito dell'attività.

Risorse aggiuntive