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.

Imagem 1 — Instagram like navigation

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.

Imagem 2 — Logo Cicerone

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:

  1. Navegar para outra tela;
  2. Voltar para uma tela anterior;
  3. Começar um novo fluxo;
  4. Voltar para uma tela específica dentro do fluxo;
  5. Entre outras operações úteis.

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
        }
    }
}
Código 1 - Configuração da Bottom Navigation

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
}
Código 2 - onCreate da MainActivity

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())
        }
    }
}
Código 3 - FragmentOneFlowContainer

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)
}
Código 4 - Componente e Módulo do 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.

Imagem 4 — Ilustração do funcionamento

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)
}
Código 5 — onPause e onResume do FlowContainerFragment

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
}
Código 6 — Interface BackButtonListener

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
    }
}
Código 7 — Implementação do onBackPressed no FlowContainerFragment

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()
    }
}
Código 8 — Implementação do onBackPressed na MainActivity

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