Simplificando rotinas com Navigation Component
Pensado para facilitar a navegação entre telas, o Navigation Component é uma das bibliotecas do Android Jetpack que vem ganhando cada vez mais espaço no cenário do desenvolvimento Android.
Neste artigo, mostraremos como realizar o desenvolvimento de um fluxo de telas com o Navigation Component num passo a passo simples e explicativo.
Antes de tudo, vamos conhecer os pontos utilizados para construir a biblioteca:
- Confirmar a transição de fragments sem lançar uma exceção (no more fragment transactions);
- Passar argumentos entre telas;
- Testar se a navegação e o retorno de dados entre telas do seu app estão ocorrendo corretamente;
- Mapear os deeplinks (links diretos) mencionados em diversos lugares do aplicativo e torná-los atualizados conforme as mudanças de navegação de seu app;
- Certificar que a navegação up and back (ida e retorno do fluxo) do app leve os usuários para lugares exatos em situações específicas;
Assim, baseado nesses pontos, a biblioteca do Navigation Component possui três fundamentos: gráfico de navegação, NavHost e NavController, explicados a seguir.
Gráfico de navegação
É o local onde estão centralizadas as informações de transição entre telas (NavHost) do aplicativo.
Apresentado em recurso XML, o gráfico em questão também possui a versão code e design, assim como os demais recursos XML.
Na imagem abaixo podemos observar a versão design com as seguintes demarcações: 1- destino (telas) e 2- ações (transição entre origem e destino).
NavHost
Em segundo lugar, temos o fundamento NavHost, container adicionado ao layout pai que mostra os diversos destinos de navegação e tem como padrão o NavHostFragment, que apresenta destinos de navegação entre fragments.
NavController
Esse é o fundamento /objeto responsável pela troca do conteúdo de destino do NavHost vigente.
Conforme ocorre a navegação pelo aplicativo, informaremos ao NavController rotas específicas de transição para que ele possa apresentar o destino adequado.
So show me the code!
Já que você já conhece os pontos e os fundamentos que foram usados para construir a biblioteca, vamos ao código!
O aplicativo apresentado aqui é chamado IterisWars e foi desenvolvido para sanar os debates intrigantes na hora do coffee break e está estruturado da seguinte forma:
- visualização de uma lista de votações em aberto (1);
- visualização de resultados — tela em desenvolvimento (2);
- informações sobre a história daquele debate (3);
- escolha do lado (4).
Para que possamos fundamentar os pontos abordados, criaremos um exemplo de implementação que relaciona alguns pontos básicos da navegação feita com o Navigation Component ao nosso projeto.
Como base, teremos o aplicativo com telas desenvolvidas para que possamos nos concentrar na navegação. Vejamos o fluxo de telas:
Antes de mais nada, observe que na imagem acima existem três tipos de transições diferentes. Mas, antes de falar sobre elas, vamos entender um pouco mais sobre as telas apresentadas:
- MainActivity – responsável por carregar elementos em comum (toolbar e BottomNavigationView);
- VotesFragment – primeira tela carregada pela MainActivity onde veremos a lista de votações;
- DevelopingFragment – futuramente será adicionado ao nosso aplicativo uma nova funcionalidade, mas enquanto isso, a estratégia adotada foi adicionar a tela “em desenvolvimento”, sendo também a segunda tela do menu inferior de navegação;
- VotingStoryFragment – ao selecionar uma votação teremos mais informações sobre ela;
- ChooseYourSideDialogFragment – Após ler sobre a votação e se interessar, o usuário poderá escolher seu lado selecionando o botão na tela de votação.
Inserindo a biblioteca no projeto
Utilizaremos a biblioteca navigation-ui-ktx para tratar UI Components, como o menu inferior ou a toolbar da aplicação.
Para a navegação entre fragments, utilizaremos a biblioteca principal necessária para integrar diversas funções de navegação, a navigation- fragment-ktx. Adotaremos a versão estável mais atual de ambas, liberada em 24 de junho de 2020.
Em primeiro lugar adicionaremos as dependências relativas ao Navigation Component em Kotlin no build.gradle do módulo app e realizaremos a sincronização do gradle (sync) com as novas dependências.
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
Adicionando gráfico de navegação
Nessa etapa, criaremos um novo diretório em nossos recursos para centralizar todos os arquivos .xml referentes à navegação e chamaremos de navigation.
Dessa maneira, ao observar o fluxo é possível perceber que, independentemente de onde está a navegação, todas as telas possuem o padrão de barra superior de ferramentas e de menu inferior.
Portanto, teremos um layout pai que carrega todas as demais telas em seu NavHost, mas que também possui os mesmos componentes visuais que são comuns entre telas.
Ou seja, se teremos um único layout padrão e só o container será alterado, para adicionar novas telas deve existir também apenas um arquivo de navegação, denominado main_navigation.xml, tratando da navegação principal dessa aplicação.
Implementando gráfico de navegação
Visto que já temos as divisões em mente, sabemos que um NavHost terá quatro destinos diferentes, logo teremos que adicionar os mesmos em nosso gráfico de navegação.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation"
app:startDestination="@id/votesFragment">
<fragment
android:id="@+id/votesFragment"
android:name="com.example.navigatecomponentbasicexample.ui.VotesFragment"
android:label="Votações"
tools:layout="@layout/fragment_votes"/>
<fragment
android:id="@+id/developingFragment"
android:name="com.example.navigatecomponentbasicexample.ui.DevelopingFragment"
android:label="Em desenvolvimento"
tools:layout="@layout/fragment_developing" />
<fragment
android:id="@+id/votingStory"
android:name="com.example.navigatecomponentbasicexample.ui.VotingStoryFragment"
android:label="História da votação"
tools:layout="@layout/fragment_voting_story"/>
<dialog
android:id="@+id/chooseYourSideDialogFragment"
android:name="com.example.navigatecomponentbasicexample.ui.ChooseYourSideDialogFragment"
android:label="História da votação"
tools:layout="@layout/dialog_choose_your_side"/>
</navigation>
Tag <navigation> – é necessário passar o atributo id da sua navegação e o startDestination, que nada mais é do que o destino inicial do seu gráfico de navegação. Nesse atributo é essencial passar o id de um destino para iniciar o fluxo nele.
Tag filho <fragment> e <dialog> – teremos quatro atributos básicos:
- ID – vale ressaltar que para telas que são adicionadas via menu de navegação (como o BottomNavigationView utilizado nesse aplicativo), é necessário que o id referente a opção do menu.xml seja igual ao id atribuído no navigation.xml;
- Name – atribuição do nome dado ao item, utilizando como padrão a convenção de nomenclatura Java-style, para tornar o nome único;
- Label – é a referência literal do nosso item. Utilizado para adicionar o nome da tela que será apresentada na toolbar;
- Layout – atributo que indica o layout da tela destino.
Então, com os destinos mapeados, adicionaremos as ações, excluindo aquelas relacionadas ao menu inferior de navegação (BottomNavigationView), que serão abordadas de outra forma.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation"
app:startDestination="@id/votesFragment">
<fragment
android:id="@+id/votesFragment"
android:name="com.example.navigatecomponentbasicexample.ui.VotesFragment"
android:label="Votações"
tools:layout="@layout/fragment_votes">
<action
android:id="@+id/action_votesFragment_to_votingStory"
app:destination="@id/votingStory" />
</fragment>
<fragment
android:id="@+id/developingFragment"
android:name="com.example.navigatecomponentbasicexample.ui.DevelopingFragment"
android:label="Em desenvolvimento"
tools:layout="@layout/fragment_developing" />
<fragment
android:id="@+id/votingStory"
android:name="com.example.navigatecomponentbasicexample.ui.VotingStoryFragment"
android:label="História da votação"
tools:layout="@layout/fragment_voting_story">
<action
android:id="@+id/action_votingStory_to_chooseYourSideDialogFragment"
app:destination="@id/chooseYourSideDialogFragment" />
</fragment>
<dialog
android:id="@+id/chooseYourSideDialogFragment"
android:name="com.example.navigatecomponentbasicexample.ui.ChooseYourSideDialogFragment"
android:label="História da votação"
tools:layout="@layout/dialog_choose_your_side">
<action
android:id="@+id/action_chooseYourSideDialogFragment_to_votesFragment"
app:destination="@id/votesFragment"/>
</dialog>
</navigation>
Tag <action> – são implementados dois atributos, o id (chave única) e destination (para onde a navegação deve levar).
Por fim, precisamos adicionar o componente visual que carregará os fragments. Dessa maneira, utilizaremos o FragmentContainerView, que estará no layout da activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottomNavigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:navGraph="@navigation/main_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/menu_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
Desse modo, essa view possuirá dois atributos principais: o defaultNavHost, que conecta o fragment ao gerenciamento de navegação do Android, cujo objetivo é implementar os princípios de navegação; e navGraph, que é o atributo em que será adicionado o nosso gráfico de navegação.
Transição A
Primeiramente falaremos sobre a utilização do componente de navegação inferior (BottomNavigationView). Reveja o fluxo:
Notam-se dois pontos importantes: a navegação pelo menu inferior e a alteração da toolbar decorrente da mudança de telas. Dessa forma, temos duas etapas de desenvolvimento:
- BottomNavigationView – primeiro iniciaremos o NavController passando o NavHost que adicionamos no activity_main.xml e passaremos seu respectivo navController a ser gerenciado.
Logo depois, realizaremos o método setupWithNavController() e passaremos o navController que deverá gerenciar a transição. Desta forma a atualização de fragments acontecerá e a navegação up and back estará atualizada conforme o destino;
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupNavigationView()
setupListeners()
}
private fun setupNavigationView() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
navController = navHostFragment.navController
bottomNavigation.setupWithNavController(navController)
}
Toolbar – nesse caso torna-se necessário adicionar o setup padrão da nossa toolbar no onCreate e utilizar o mesmo navController configurado para gerenciar a troca de destinos no NavHost vigente.
Então, iremos alterar o título de cada destino (por meio do atributo label) e, se o destino vigente possuir uma tela anterior na pilha de navegação, será adicionado o ícone de navegação na toolbar, mostrando que é possível retornar para o elemento anterior da pilha.
Todos esses comportamentos são incluídos através do método setupActionBarWithNavController();
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(toolbar)
setupNavigationView()
setupListeners()
}
private fun setupNavigationView() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
bottomNavigation.setupWithNavController(navController)
}
private fun setupListeners() {
toolbar.setNavigationOnClickListener { super.onBackPressed() }
}
Transição B
Em seguida, na transição B observamos o fluxo mais simples do aplicativo: trânsito entre telas por botões sem passagem de argumentos.
Vejamos o fluxo isoladamente:
Para cada clique no aplicativo relacionado a transição de telas, será necessário adicionar o método navigate() e dentro dele passar o id da ação que adicionamos no nosso gráfico de navegação. A implementação dessas ações ficará da seguinte forma:
private fun setupListener() {
card.setOnClickListener {
findNavController().navigate(R.id.action_votesFragment_to_votingStory)
}
}
private fun setupListener() {
chooseButton.setOnClickListener {
findNavController().navigate(R.id.action_votingStory_to_chooseYourSideDialogFragment)
}
}
- Se houver necessidade de passar argumentos para frente, o navigate() também receberá como parâmetro o bundle que deseja passar para o destino.
- Caso queira simplificar a expressão por entender que uma navegação na maioria das vezes estará associada a um clique, segue uma dica de extensão para reduzir o esforço de implementação em sua Fragment:
fun View.clickAndNavigate(fragment: Fragment, destination: Int) =
this.setOnClickListener { fragment.findNavController().navigate(destination)
}
private fun setupListener() {
card.clickAndNavigate(this, R.id.action_votesFragment_to_votingStory)
}
Transição C
A transição C poderia ser realizada junto com a transição anterior, porém, para que possamos facilitar o entendimento do fluxo de navegação iremos mostrá-la separadamente.
Seja como for, ao adicionar a ação do ChooseYourSideDialogFragment para VotesFragment, teremos uma implementação idêntica as ações da transição B.
A única diferença é que o navigate(), por si só, não apaga a existência das telas anteriores que estão ainda na pilha de navegação.
Se nesse caso optarmos por seguir a mesma implementação na ação de voltar da tela, teríamos ainda o fluxo salvo na pilha, recriando o fluxo de votação toda vez que o usuário entrasse na rota de votação novamente, aumentando cada vez mais a pilha de navegação.
Na próxima imagem, a pilha da esquerda representa a navegação até o momento em que chegamos no ChooseYourSideDialogFragment. Já, na pilha da direita, a imagem demonstra a adição do elemento em caso de utilização da mesma implementação.
Por fim, na imagem abaixo, temos o fluxo atual (pilha equerda) e o fluxo esperado (pilha direita) com a nova implementação que utilizaremos.
Para realizar o fluxo desejado, iremos alterar um pouco a ação de navegação no main_navigation.xml. Veja:
<dialog
android:id="@+id/chooseYourSideDialogFragment"
android:name="com.example.navigatecomponentbasicexample.ui.ChooseYourSideDialogFragment"
android:label="História da votação"
tools:layout="@layout/dialog_choose_your_side">
<action
android:id="@+id/action_chooseYourSideDialogFragment_to_votesFragment"
app:destination="@id/votesFragment"
app:popUpTo="@id/votesFragment"
app:popUpToInclusive="true" />
</dialog>
Nessa ação adicionamos dois atributos para realizar a ação de navegar de volta para VotesFragment:
O primeiro é o popUpTo, que funciona da mesma forma que o destination. A diferença entre eles está na indicação do destino que deverá ser adicionado na pilha que, nesse caso, informará para qual destino a navegação deve retornar, excluindo automaticamente os destinos posteriores a ele, até retornar a VotesFragment.
No aplicativo usado como exemplo, é necessário recriar a página principal para que, após a votação do usuário, ela seja atualizada.
Logo, o segundo atributo é o popUpToInclusive, que recria a tela de destino, passando novamente pelo ciclo de vida de criação do Fragment.
Considerações
Enfim, por meio de todo esse processo conseguimos implementar a navegação nessa pequena aplicação e entender um pouco mais sobre o uso do componente.
Assim sendo, vale lembrar que o Navigation Component recebe constantes atualizações, passando por diversas melhorias que o tornam cada vez mais atraentes ao olhar dos desenvolvedores.
Ou seja, sempre se faz necessário olhar caso por caso, avaliando as necessidade e particularidade de cada projeto. No caso de projetos extensos que não utilizam o Navigation Component, existe o risco de tornar a refatoração densa, ainda mais com a adição de gráficos de navegação via XML.
Em contrapartida, adicionar um gráfico pode render ganho de velocidade no quesito de manutenções futuras por centralizar a lógica de navegação em um único lugar.
Outro fator muito importante é entender os componentes visuais que seu aplicativo tem. Muitas vezes views componentizadas específicas podem apresentar mais trabalho na hora de implementar a navegação, mas é evidente que o uso deste componente torna os princípios de navegação menos complexos a cada dia que passa.
Por fim, caso ainda tenha alguma dúvida sobre esse ou demais assuntos relacionados, acesse a documentação oficial do Navigation Component.