Introdução
Nos aplicativos que desenvolvo, gosto de usar o modelo “Instagram like” de navegação, que consiste em manter o estado da tela sempre que há uma mudança de aba. Para esclarecer esse conceito, imagine que você está utilizando um app com uma Bottom Navigation Bar que possui duas abas, a primeira exibe uma lista de filmes, a outra uma lista de séries. Ao utilizar o app, você chega na metade da primeira lista e resolve trocar de aba pois está cansado de ver filmes. Após a trocar de abas e passar um tempo na lista de séries, você retorna para a aba dos filmes e a lista deve estar na mesma posição de quando você saiu. A imagem 1 ilustra esse comportamento.

Para essa postagem, utilizei esse artigo ¹como base, que apesar de possuir um código desatualizado, é extremamente bem escrito e rico em conhecimento, confiram! Estou utilizando uma biblioteca de terceiros ao invés do NavigationComponent pelo simples fato de que, até o presente momento, não é possível criar esse comportamento sem utilizar de técnicas de caráter duvidoso (assim como é feito no exemplo disponibilizado pela Google). Todo o código está disponível aqui. Nas próximas seções serão apresentados os dois frameworks utilizados, Dagger2 e Cicerone.
Dagger
Dagger 2 é um framework, gerido pela Google, e que possibilita a Injeção de Dependência (DI) dentro de um projeto Android. Caso haja interesse sobre o assunto, também temos um post sobre Injeção de Dependências e Dagger2! Você pode encontra-lo aqui.
Cicerone
Cicerone é uma biblioteca utilizada para auxiliar a navegação entre telas em projetos Android. Essa lib foi criada para ser usada com a arquitetura Model-View-Presenter (MVP), porém é maleável o suficiente para ser utilizada com qualquer arquitetura.

Algumas das vantagens de utilizar o Cicerone são: não está preso aos Fragments, lifecycle-safe, fácil para utilizar com testes unitários, enfim, só coisa boa! Além de tudo isso, é muito fácil de configurar e utilizar, fazendo com que a tarefa de navegar entre telas (animando, ou não) esteja a um comando de distância.
Algumas funções disponíveis no Cicerone:
- Navegar para outra tela;
- Voltar para uma tela anterior;
- Começar um novo fluxo;
- Voltar para uma tela específica dentro do fluxo;
- Entre outras operações úteis.
Navegação “Instagram like”
Antes de começar esta seção, tenha certeza de ter lido a documentação do Cicerone, e entendido como utiliza-lo.
Agora que sabemos quais tecnologias utilizar, e qual o comportamento esperado, chegou a tão esperada hora de ver um pouco de código!
Como sabemos, o Android faz uso de pilhas para controlar os fluxos de tela dentro dos apps, isso quer dizer que no topo dessa pilha está a tela visível ao usuário. Se a ação de “voltar” é acionada, há um pop no topo , e o novo topo representa a nova tela visível, caso a pilha fique vazia após o pop, o app é fechado.
Fica fácil perceber que gerenciar as dezenas de possíveis telas (de várias abas diferentes) é uma tarefa bem complicada quando utilizamos apenas uma pilha. Logo, para nos auxiliar, iremos usar uma pilha por aba! Ou seja, cada aba possui uma pilha diferente e independente.
No código de exemplo, para fins de simplificação, a MainActivity irá conter a Bottom Navigation e um contêiner para os Fragments. A ideia é que os Fragments, que são filhos do FlowContainerFragment (e será explicado abaixo), de cada aba sejam instanciados, e adicionados, dentro da MainActivity, e sempre que uma mudança de aba acontecer, há um hide dos Fragments não ativos. Caso você não tenha entendido, vamos olhar o código!
private fun setupNavActions(){
supportFragmentManager.beginTransaction()
.add(R.id.mainFlowContainer, fragmentOne, fragmentOneTag)
.add(R.id.mainFlowContainer, fragmentTwo, fragmentTwoTag)
.commitNow()
//Cliques na bottomNavigation
bottomNavigationView.setOnNavigationItemSelectedListener {
onNavigationItemSelected(it)
}
}
//Navegação na bottomNavigation, retorna true se o item foi selecionado, false cc
private fun onNavigationItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.navigation_home -> {
supportFragmentManager
.beginTransaction()
.hide(fragmentTwo)
.show(fragmentOne)
.commit()
activeFragmentTag = fragmentOne.tag
true
}
R.id.navigation_dashboard -> {
supportFragmentManager
.beginTransaction()
.hide(fragmentOne)
.show(fragmentTwo)
.commit()
activeFragmentTag = fragmentTwo.tag
true
}
else -> {
false
}
}
}
No código 1, existem duas abas (Home e Nav), a primeira possui um Fragment chamado FragmentOne como tela inicial, já a segunda possui o FragmentTwo. A função setupNavActions() é responsável por adicionar ambos os Fragments ao contêiner citado anteriormente, além de ser responsável por adicionar ações de toque nas abas. Caso a aba Home seja selecionada, o fragmentTwo é escondido, enquanto o fragmentOne é exibido, caso a outra aba seja selecionado, basta inverter a lógica de exibição. Utilizando essa estratégia, é possível manter as abas independentes, com fluxos próprios, preservando os respectivos estados.Código 2 — onCreate da MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavActions()
supportFragmentManager
.beginTransaction()
.hide(fragmentTwo)
.show(fragmentOne)
.commit()
activeFragmentTag = fragmentOne.tag
}
Toda a configuração da Bottom Navigation é feita dentro do onCreate da MainActivity, como pode ser visto no código 2, e uma vez configurada, a primeira aba é exibida. Também seria possível utilizar o Cicerone para realizar essas configurações iniciais (como foi feito no artigo original), porém, acho que essa abordagem é menos legível e didática, portanto não utilizei-a. Agora que já falamos da MainActivity, vamos brincar com os Fragments, Cicerone e Dagger!
Para evitar duplicação de código, a classe FlowContainerFragment foi criada (e todo novo Fragment correspondente a uma aba da Bottom Navigation deverá ser filho de FlowContainerFragment). A injeção e configuração do Cicerone ocorrerão dentro do Fragment pai, que possui (dentro de seu layout) um contêiner que abrigará as telas do fluxo. O código abaixo exemplifica como a herança será usada pelos Fragments que serão utilizados nas abas. Nesse caso, o FragmentOneFlowContainer abriga o FragmentOne (como pode ser visto na linha 10 do código 3), e um processo similar é feito com o FragmentTwo.
class FragmentOneFlowContainer : FlowContainerFragment() {
companion object {
fun newInstance() = FragmentOneFlowContainer()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
cicerone.router.replaceScreen(FragmentOneScreen())
}
}
}
Como sabemos, o Dagger precisa saber o que, e como, injetar, portanto é preciso criar os componentes e módulos utilizados pela DI. No projeto de exemplo, tanto o componente como o módulo estão contidos no arquivo FlowContainerDI.
@Module
class FlowContainerModule(
private val frameActivity: FragmentActivity,
private val fragmentManager: FragmentManager
) {
@Provides
@PerFlow
fun getNavigator(): Navigator {
return SupportAppNavigator(frameActivity, fragmentManager, R.id.flowContainer)
}
@Provides
@PerFlow
fun getCicerone(): Cicerone<Router> = Cicerone.create()
@Provides
@PerFlow
fun getNavigationHolder(cicerone: Cicerone<Router>): NavigatorHolder = cicerone.navigatorHolder
@Provides
@PerFlow
fun getRouter(cicerone: Cicerone<Router>): Router = cicerone.router
}
@Component(modules = [FlowContainerModule::class])
@PerFlow
interface FlowContainerComponent {
fun getCicerone(): Cicerone<Router>
fun getNavigationHolder(): NavigatorHolder
fun getRouter(): Router
fun inject(flowContainerFragment: FlowContainerFragment)
}
Basicamente, dentro do módulo, ensinamos ao Dagger como criar o Cicerone (e todos os componentes necessários para sua utilização), e também configuramos o Navigator , que utilizará o contêiner do FlowContainerFragment. Também fazemos uso de Qualifiers, possibilitando que cada novo Fragment descendente de FlowContainerFragment tenha uma instância diferente do Cicerone.

Resumindo o que foi visto nessa seção, existem dois contêineres, o externo que pertence à MainActivity, e o Interno que pertence ao FlowContainerFragment. O contêiner externo abriga os contêineres internos, possibilitando que haja a mudança de abas sem que a BottomNavigationBar desapareça. Já o contêiner interno — FlowContainer — abriga os Fragments dos fluxos criados por cada aba, logo, cada aba possui o próprio FlowContainer. Ou seja, sempre que há uma mudança de aba, o FlowContainer que está visível muda, possibilitando que haja preservação do estado das abas.
onPause e onResume
Com a configuração do Dagger completa, podemos realizar os ajustes finais do FlowContainerFragment! É importante que, no onPause, o Navegador seja removido do Cicerone (assim garantimos que nenhum comando será recebido em momentos não apropriados), já no onResume, inserimos o Navegador novamente.
override fun onPause() {
super.onPause()
this.navigatorHolder.removeNavigator()
}
override fun onResume() {
super.onResume()
this.navigatorHolder.setNavigator(this.navigator)
}
Back Button
Por último, mas não menos importante, é preciso tratar as ações do botão “voltar” do Android! Para tanto, criaremos uma interface para o tratamento dessa ação, e todo Fragment deverá implementa-la!
interface BackButtonListener {
fun onBackPressed(): Boolean
}
No nosso caso, tanto o FlowContainerFragment, quanto os Fragments que serão utilizados por ele (FragmentOne, e FragmentTwo), deverão implementar o BackButtonListener. No FlowContainerFragment , devemos garantir que, caso o Fragment visível execute a ação de voltar, ele tenha implementado a interface, e que o onBackPressed foi acionado. Já nos FragmentOne e FragmentTwo, só é necessário que o Cicerone “volte uma tela”.
override fun onBackPressed(): Boolean {
return if (isAdded) {
val childFragment = childFragmentManager
.findFragmentById(R.id.flowContainer)
childFragment != null &&
childFragment is BackButtonListener &&
childFragment.onBackPressed()
} else {
false
}
}
A MainActivity não precisa implementar a nossa interface, uma vez que já possui o método onBackPressed. Nela, apenas conferimos se algum comportamento não esperado aconteceu ao realizar a ação de voltar, caso tenha, apenas finalizamos a activity.
override fun onBackPressed() {
val currentFragmentMoviesFlow (supportFragmentManager.findFragmentByTag(activeFragmentTag) as? BackButtonListener)
currentFragmentMoviesFlow?.let {
if (!it.onBackPressed())
finish()
}
}
Conclusão
A navegação “Instagram like” foi explicada, exemplificada e implementada nesse post. Após a leitura, você deve ser capaz de criar sua própria Bottom Navigation, utilizando Cicerone e Dagger, com uma navegação “Instagram like”.
Se você chegou até aqui, parabéns! Se houver quaisquer críticas, sugestões, ou adições sobre o conteúdo desse capítulo, sinta-se livre sugeri-las! Lembrando que todo o projeto de exemplo poder ser encontrado aqui! Espero que tenha sido útil, muito obrigado pela atenção, até o próximo! ;)
Referências
1 https://medium.com/@yurimachioni/creating-an-instagram-like-flow-using-cicerone-and-dagger2-bottomnavigation-with-fragments-777771ff4401
2 https://github.com/google/dagger
3 https://github.com/terrakok/Cicerone
4 https://blog.f22labs.com/instagram-like-bottom-tab-fragment-transaction-android-389976fb8759