Jetpack Navigation em projetos multi-module

Nelson Glauber
6 min readFeb 4, 2021

A biblioteca Jetpack Navigation traz diversos benefícios na implementação do fluxo navegacional de aplicações Android. Mas para ser bem honesto, mesmo sabendo de todos os seus benefícios, eu nunca fui muito fã dela e sempre evitei utilizá-la. Nem sei explicar o porquê, mas uma coisa que me incomoda é não ter nos fragments a animação de transição padrão de activities existente na plataforma. É preciso utilizar animações do tipo slide, fade, etc… Até pensei que usar a animação padrão do S.O. seria algo simples que o Google poderia fazer, mas conversando uma vez com o

, ele me falou que cada fabricante implementa sua própria animação, então não teria como a API de Navigation utilizar ou ter acesso a esse recurso. Enfim, é uma coisa boba, mas é uma limitação.

No projeto que estou prestes a começar, foi definido que o Jetpack Navigation será utilizado. Mas uma outra premissa é que cada feature seja implementada em um módulo separado (futuramente tentaremos utilizar feature modules, caso necessário). Nesse artigo apresentarei minha sugestão de utilização dessa biblioteca em um projeto com múltiplos módulos.

Uma solução Android Driven

A solução apresentada aqui utiliza puramente recursos do Android, e que permite facilmente adicionar abstrações sobre ela. Provavelmente é isso que farei ao longo do projeto, mas quero deixar registrado a idéia base e simplificada aqui.

Para demonstrar essa solução foi criado um exemplo que tem os seguintes módulos:

Dependência entre os módulos da aplicação.
  • app: é o módulo raiz da aplicação. A idéia é que esse esse projeto siga a abordagem de single activity, então aqui só ficará a MainActivity, as demais telas ficarão nos seus respectivos módulos. Perceba que esse módulo possui um navigation graph (app_nav_graph.xml) que será explicado na próxima seção.
Estrutura do módulo app.
  • feature_onboarding: contém as funcionalidades relativas ao "embarque" do usuário na aplicação. Nesse exemplo, o WelcomeFragment chama o HomeFragment. Isso é feito no navigation graph onboarding_nav_graph.xml.
Estrutura do modulo onboarding.
  • feature_profile: tem o propósito de exibir e configurar as informações do usuário. Nesse exemplo, o ProfileFragment chama o DefinePasswordFragment. Isso é definido no navigation graph profile_nav_graph.xml.
Estrutura do módulo profile

As funcionalidades dos módulos são irrelevantes para o propósito deste artigo. O que importa aqui é que app depende dos dois outros módulos.

Iniciando a navegação

A configuração inicial da navegação é feita definindo um NavHost no arquivo de layout da aplicação (activity_main.xml).

<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/app_nav_graph"
.../>

Na propriedade app:navGraph, que indica o navigation graph inicial foi utilizado o arquivo app_nav_graph.xml listado a seguir:

<navigation 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_nav_graph"
app:startDestination="@id/onboarding_nav_graph">

<include app:graph="@navigation/onboarding_nav_graph" />

<include app:graph="@navigation/profile_nav_graph" />


</navigation>

Esse navigation graph simplesmente aponta para os navigation graphs dos outros módulos utilizando a tag <include> e determina que o fluxo inicial é o do onboarding por meio da propriedade app:startDestination.

Como navegar para o fluxo de outro módulo?

A navegação entre telas do mesmo módulo é muito simples. Vejamos o navigation graph do módulo de onboarding.

<!-- OnBoarding Navigation Graph onboarding_nav_graph.xml -->
<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/onboarding_nav_graph"
app:startDestination="@id/welcomeFragment">

<fragment
android:id="@+id/welcomeFragment"
android:name="br.com.nglauber.feature_onboarding.WelcomeFragment"
android:label="fragment_welcome"
tools:layout="@layout/fragment_welcome">
<action
app:popUpTo="@id/onboarding_nav_graph"
android:id="@+id/action_welcomeFragment_to_homeFragment"
app:destination="@id/homeFragment" />
</fragment>
<fragment
android:id="@+id/homeFragment"
android:name="br.com.nglauber.feature_onboarding.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_profile"
app:destination="@id/profile_nav_graph" />
</fragment>
</navigation>

Perceba que no WelcomeFragment foi declarado uma <action> para exibir o HomeFragment. No código, essa chamada é feita da seguinte forma:

findNavController().navigate(
R.id.action_welcomeFragment_to_homeFragment
)

Mas note que existe uma outra <action> para exibir a tela de profile… Nela, o app:destination é definido com o valor profile_nav_graph. Mas esse id, é o id do navigation graph do módulo de profile. 😕 Como é possível usar um id de um outro módulo? 🤔

Basta definir uma referência do id no seu módulo como a seguir:

<!-- res/values/ids.xml -->
<resources>
<item name="profile_nav_graph" type="id" />
</resources>

Mas o que esse id significa? Basicamente esse id é criado pelo compilador e pode ser reutilizado por toda a aplicação. Neste exemplo, esse mesmo id é usado no navigation graph do módulo profile como pode ser visto a seguir:

<!-- Profile Navigation Graph profile_nav_graph.xml -->
<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/profile_nav_graph"
app:startDestination="@id/profileFragment">

<fragment
android:id="@+id/profileFragment"
android:name="br.com.nglauber.feature_profile.ProfileFragment"
android:label="fragment_profile"
tools:layout="@layout/fragment_profile">
<action
android:id="@+id/action_profileFragment_to_definePasswordFragment"
app:destination="@id/definePasswordFragment" />
</fragment>
<fragment
android:id="@+id/definePasswordFragment"
android:name="br.com.nglauber.feature_profile.DefinePasswordFragment"
android:label="fragment_define_password"
tools:layout="@layout/fragment_define_password">
<deepLink
android:id="@+id/deepLink"
app:uri="ngvl://passwordupdate" />
</fragment>
</navigation>

Com isso, no HomeFragment (do módulo onboarding), é possível chamar a action normalmente:

findNavController().navigate(R.id.action_profile)

Como voltar para um fragment de um módulo diferente?

Para voltar para a tela anterior utilizando a navigation API é preciso apenas a seguinte chamada:

findNavController().popBackStack()

E se quiser voltar para um ponto específico da navegação, deve-se usar:

findNavController().popBackStack(R.id.fragment_id, false)

Sendo que o id informado como parâmetro deve ser o id do fragment até onde o retorno deve acontecer. Por exemplo, dada as telas A > B > C > D > E, para voltar da tela E para B, o id a ser informado deve ser o B.

Mas e se esse fragment estiver em outro módulo? Vejamos o seguinte fluxo a seguir:

Nesse fluxo, a idéia é que, ao pressionar um botão no DefinePasswordFragment, o HomeFragment seja exibido.

A solução é a mesma da seção anterior. Basta definir uma referência para o id do fragment no módulo onboarding…

<resources>
<item name="homeFragment" type="id" />
</resources>

E assim, utilizar o seguinte código no DefinePasswordFragment:

findNavController().popBackStack(R.id.homeFragment, false)

Lembrando que o homeFragment está definido no navigation graph do módulo profile (profile_nav_graph.xml).

Como navegar para um fragment de outro módulo que não seja o fragment inicial?

No exemplo onde o HomeFragment exibe o ProfileFragment, na verdade o HomeFragment exibe o profile_nav_graph que é o id do navigation graph do módulo profile, que por sua vez, indica que o ProfileFragment é a tela inicial. Mas e se, da HomeFragment for preciso exibir a DefinePasswordFragment?

Na documentação do Jetpack Navigation temos a seguinte citação:

[Nested graphs] also provide a level of encapsulation — destinations outside of the nested graph do not have direct access to any of the destinations within the nested graph.

Ou seja, não é possível navegar para um ponto específico de um outro navigation graph. Mas há uma exceção: deep links!

Observe que foi definido um <deepLink> na declaração do DefinePasswordFragment onde é informado que qualquer link com a URI ngvl://passwordupdate invocará esse fragment (“ngvl” são minhas iniciais, aqui poderia ser qualquer schema). Sendo assim, para ir do HomeFragment para o DefinePasswordFragment basta usar:

findNavController().navigate(Uri.parse("ngvl://passwordupdate"))

A Navigation API identificará automaticamente o link e redirecionará para o DefinePasswordFragment.

Conclusão

Foi possível observar nesse artigo que é relativamente simples utilizar a Navigation API em diferentes módulos, criando um navigation graph para cada um deles. A utilização de ids de referência existe desde a primeira versão do Android e é usada bastante na definição de temas, mas pouco usada em outras situações. Entretanto, nesse cenário de navegação multi-módulos pode ser bem útil.

Como mencionei no início do post, essa abordagem permite várias melhorias, como por exemplo: criar um módulo separado apenas para armazenar os ids de navegação e URIs de deep links, e fazer que todos os features modules usem esse módulo, evitando assim que ids repetidos tenham que ser definidos em cada módulo; outra melhoria também poderia ser criar uma abstração sobre o findNavigationController() e injetar esse objeto via DI. Enfim, a idéia aqui foi passar uma base para a solução, mas a customização fica ao seu critério.

Um ponto que pode ser considerado negativo no uso de nested graphs é que não é possível usar SafeArgs entre módulos (no próprio módulo pode ser usado normalmente), mas a passagem de parâmetros pode ser feita via Bundle sem problema.

Por fim, a própria API de Navigation dá suporte para navegação utilizando feature modules, então caso você esteja planejando utilizar esse recurso no seu app, as coisas são ainda mais fáceis do que as que eu apresentei aqui.

É isso… Espero que gostem e qualquer dúvida, é só entrar em contato.

4br4ç05,
nglauber

--

--

Nelson Glauber

Android Developer. Google Developer Expert for Android/Kotlin. Author of “Dominando o Android”.