Criar navegação responsiva

A navegação é a interação do usuário com a interface de um aplicativo para acessar destinos de conteúdo. Os princípios de navegação do Android oferecem diretrizes que ajudam a criar uma navegação consistente e intuitiva para o app.

As IUs responsivas/adaptáveis fornecem destinos de conteúdo responsivo e geralmente incluem diferentes tipos de elementos de navegação em resposta às mudanças de tamanho da tela. Por exemplo, uma barra de navegação na parte de baixo de telas pequenas, uma coluna de navegação em telas de tamanho médio ou uma gaveta de navegação persistente em telas grandes. No entanto, as interfaces responsivas/adaptáveis ainda precisam estar em conformidade com os princípios de navegação.

O componente de navegação do Jetpack implementa os princípios de navegação e facilita o desenvolvimento de apps com interfaces responsivas/adaptativas.

Figura 1. Telas expandidas, médias e compactas com gaveta, coluna e barra inferior de navegação.

Navegação de IU responsiva

O tamanho da janela de exibição ocupada por um app afeta a ergonomia e a usabilidade. As classes de tamanho de janela permitem determinar elementos de navegação adequados (como barras, colunas ou gavetas de navegação) e os colocar onde ficam mais acessíveis ao usuário. Nas diretrizes de layout do Material Design, os elementos de navegação ocupam um espaço persistente na borda de cima da tela e podem se mover para a borda de baixo quando a largura do app é compacta. A escolha dos elementos de navegação depende muito do tamanho da janela do app e do número de itens que o elemento precisa conter.

Classe de tamanho da janela Poucos itens Muitos itens
largura compacta barra de navegação de baixo gaveta de navegação (borda de cima ou de baixo)
largura média coluna de navegação gaveta de navegação (borda superior)
largura expandida coluna de navegação gaveta de navegação persistente (borda de cima)

Os arquivos de recursos de layout podem ser qualificados por pontos de interrupção de classes de tamanho da janela para usar elementos de navegação diferentes em diferentes dimensões de exibição.

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

Destinos de conteúdo responsivo

Em uma interface responsiva, o layout de cada destino de conteúdo se adapta às mudanças no tamanho da janela. O app pode ajustar o espaçamento do layout, reposicionar elementos, adicionar ou remover conteúdo ou mudar elementos da IU, incluindo elementos de navegação.

Quando cada destino individual processa eventos de redimensionamento, as mudanças são isoladas na interface. O restante do estado do app, incluindo a navegação, não é afetado.

A navegação não pode ocorrer como um efeito colateral das mudanças de tamanho da janela. Não crie destinos de conteúdo apenas para acomodar tamanhos de janela diferentes. Por exemplo, não crie destinos de conteúdo diferentes para as telas de um dispositivo dobrável.

A navegação para destinos de conteúdo como efeito colateral das mudanças de tamanho da janela apresenta os seguintes problemas:

  • O destino antigo (para o tamanho anterior da janela) pode ficar temporariamente visível antes de navegar ao novo destino.
  • Para manter a reversibilidade, por exemplo, quando um dispositivo está dobrado e desdobrado, a navegação é necessária para cada tamanho de janela.
  • A manutenção do estado do aplicativo entre destinos pode ser difícil, já que a navegação pode destruir o estado ao destacar a backstack.

Além disso, talvez o app não esteja em primeiro plano quando as mudanças no tamanho da janela acontecerem. O layout do app pode exigir mais espaço do que o app em primeiro plano. Quando o usuário volta para o app, a orientação e o tamanho da janela podem ter mudado.

Caso o app exija destinos de conteúdo exclusivos com base no tamanho da janela, considere combinar os destinos relevantes em um único destino que inclua layouts alternativos e adaptáveis.

Destinos de conteúdo com layouts alternativos

Como parte de um design responsivo/adaptável, um único destino de navegação pode ter layouts alternativos, dependendo do tamanho da janela do app. Cada layout ocupa a janela inteira, mas layouts diferentes são apresentados para diferentes tamanhos de janela (design adaptável).

Um exemplo canônico é a visualização de detalhes de lista (link em inglês). Em janelas compactas, o app mostra um layout de conteúdo para a lista e outro para os detalhes. Navegar até o destino da visualização de detalhes e listas mostra inicialmente apenas o layout da lista. Quando um item da lista é selecionado, o app mostra o layout detalhado, substituindo a lista. Quando o controle "Voltar" é selecionado, o layout da lista é mostrado, substituindo o detalhe. No entanto, para tamanhos de janela expandidos, os layouts de lista e detalhes são exibidos lado a lado.

A classe SlidingPaneLayout permite criar um único destino de navegação com dois painéis de conteúdo lado a lado em telas grandes, mas apenas um painel por vez em telas pequenas, como em smartphones convencionais.

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

Consulte Criar um layout de dois painéis para detalhes sobre a implementação de um layout de detalhes e listas usando SlidingPaneLayout.

Um gráfico de navegação

Para oferecer uma experiência do usuário consistente em qualquer tamanho de dispositivo ou janela, use um único gráfico de navegação em que o layout de cada destino de conteúdo seja responsivo.

Se você usar um gráfico de navegação diferente para cada classe de tamanho de janela, sempre que o app fizer a transição de uma classe de tamanho para outra, vai ser necessário determinar o destino atual do usuário nos outros gráficos, construir uma backstack e reconciliar as informações de estado diferentes entre os gráficos.

Host de navegação aninhado

O app pode incluir um destino de conteúdo que tenha destinos próprios. Por exemplo, em um layout de detalhes e listas, o painel de detalhes do item pode incluir elementos da interface que vão para o conteúdo que substitui o detalhe do item.

Para implementar esse tipo de subnavegação, torne o painel de detalhes um host de navegação aninhado com um gráfico de navegação próprio que especifica os destinos acessados no painel de detalhes:

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

Isso é diferente de um gráfico de navegação aninhado, porque o gráfico de navegação do NavHost aninhado não está conectado ao gráfico de navegação principal. Ou seja, não é possível navegar diretamente dos destinos em um gráfico para destinos no outro.

Para mais informações, consulte Gráficos de navegação aninhados.

Estado preservado

Para fornecer destinos de conteúdo responsivo, o app precisa preservar o estado quando o dispositivo é girado, dobrado ou quando a janela do app é redimensionada. Por padrão, mudanças de configuração como essas recriam as atividades, os fragmentos e a hierarquia de visualização do app. A maneira recomendada de salvar o estado da IU é com um ViewModel, que sobrevive às mudanças de configuração. Consulte Salvar estados da interface .

As mudanças de tamanho precisam ser reversíveis, por exemplo, quando o usuário gira o dispositivo e depois volta à posição original.

Os layouts responsivos/adaptáveis podem mostrar conteúdo diferente em diferentes tamanhos de janela. Assim, os layouts responsivos geralmente precisam salvar mais estados relacionados ao conteúdo, mesmo que ele não se aplique ao tamanho atual da janela. Por exemplo, um layout pode ter espaço para mostrar um widget de rolagem extra apenas em larguras de janela maiores. Se um redimensionamento deixar a largura da janela muito pequena, o widget será ocultado. Quando o app for redimensionado para as dimensões anteriores, o widget de rolagem vai ficar visível novamente, e a posição de rolagem original será restaurada.

Escopos do ViewModel

O guia para desenvolvedores sobre como Migrar para o componente de navegação prescreve uma arquitetura de atividade única em que os destinos são implementados como fragmentos e os modelos de dados deles são implementados usando ViewModel.

Um ViewModel tem o escopo definido para um ciclo de vida e, quando esse ciclo termina permanentemente, o ViewModel é liberado e pode ser descartado. O ciclo de vida que tem o escopo ViewModel e, portanto, a amplitude de compartilhamento do ViewModel, depende da delegação de propriedade usada para acessar o ViewModel.

No caso mais simples, cada destino de navegação é um único fragmento com um estado de IU completamente isolado. Assim, cada fragmento pode usar o delegação de propriedade viewModels() para ter um ViewModel com escopo para o fragmento.

Para compartilhar o estado da IU entre fragmentos, defina o escopo do ViewModel para a atividade chamando activityViewModels() nos fragmentos. O equivalente para Activity é apenas viewModels(). Isso permite que a atividade e os fragmentos anexados a ela compartilhem a instância ViewModel. No entanto, em uma arquitetura de atividade única, esse escopo ViewModel tem a duração do app, de modo que o ViewModel permaneça na memória mesmo que não seja usado por nenhum fragmento.

Suponha que o gráfico de navegação tenha uma sequência de destinos de fragmento que representa um fluxo de finalização da compra e que o estado atual de toda a experiência de finalização de compra esteja em um ViewModel compartilhado entre os fragmentos. O escopo do ViewModel para a atividade não é apenas amplo demais, mas também expõe outro problema: se o usuário passa pelo fluxo de finalização de compra de um pedido e faz isso novamente para um segundo pedido, ambos os pedidos usam a mesma instância do ViewModel de finalização da compra. Antes da segunda finalização de pedido, é necessário limpar os dados do primeiro pedido manualmente. Qualquer erro pode ser caro para o usuário.

Em vez disso, defina o escopo do ViewModel para um gráfico de navegação no NavController atual. Crie um gráfico de navegação aninhado para encapsular os destinos que fazem parte do fluxo de finalização de compra. Em seguida, em cada um desses destinos do fragmento, use o delegado de propriedade navGraphViewModels() e transmita o ID do gráfico de navegação para extrair o ViewModel compartilhado. Isso garante que, quando o usuário sair do fluxo de finalização da compra e o gráfico de navegação aninhado estiver fora do escopo, a instância correspondente do ViewModel seja descartada e não seja usada na próxima finalização.

Escopo Delegado de propriedade Pode compartilhar o ViewModel com
Fragmento Fragment.viewModels() Somente fragmento
Atividade Activity.viewModels() ou Fragment.activityViewModels() A atividade e todos os fragmentos anexados a ela
Gráfico de navegação Fragment.navGraphViewModels() Todos os fragmentos no mesmo gráfico de navegação

Se você estiver usando um host de navegação aninhado (consulte a seção Host de navegação aninhado), os destinos nesse host não podem compartilhar instâncias de ViewModel com destinos fora do host ao usar navGraphViewModels(), porque os gráficos não estão conectados. Nesse caso, use o escopo da atividade.

Outros recursos