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