Jetpack Navigation em projetos multi-module
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 Ian Lake, 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:
- 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.
- feature_onboarding: contém as funcionalidades relativas ao "embarque" do usuário na aplicação. Nesse exemplo, o
WelcomeFragment
chama oHomeFragment
. Isso é feito no navigation graphonboarding_nav_graph.xml
.
- feature_profile: tem o propósito de exibir e configurar as informações do usuário. Nesse exemplo, o
ProfileFragment
chama oDefinePasswordFragment
. Isso é definido no navigation graphprofile_nav_graph.xml
.
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