Navegação: navegação condicional

Tempo de leitura: 4 minutes

Introdução

No artigo anterior, usei a NavigationUI, implementei a navegação inferior no aplicativo e também adicionei um SelectionFragment para habilitar ou desabilitar o rastreamento do café. No entanto, independentemente de desabilitarmos ou habilitarmos o rastreador de café, os usuários ainda podem navegar até o fragmento CoffeeList, o que não parece muito certo.

Neste artigo, corrigirei isso adicionando navegação condicional e orientando nossos usuários a fazer uma seleção ao iniciar o aplicativo pela primeira vez. Vou usar a API do Datastore para persistir a seleção do usuário e usá-la para decidir a exibição do destino da coffeeList na navegação inferior.

 

Preparando o aplicativo para navegação condicional

Aqui está uma rápida revisão das alterações que fiz desde o último artigo.

Se você quiser revisar as alterações, pode verificar o repositório aqui. Você também pode verificar o código deste repositório se quiser acompanhar!

O aplicativo agora pode estar em 3 estados diferentes.

  • DONUT_ONLY, o que significa que o usuário desativou a funcionalidade de rastreamento de café
  • DONUT_AND_COFFEE, o que significa que o usuário deseja acompanhar o consumo de Donut e Café
  • NOT_SELECTED, o que significa que o usuário ainda não fez uma seleção e possivelmente está executando o aplicativo pela primeira vez ou talvez ele apenas esteja com dificuldade para se decidir

 

Implementando Navegação Condicional

Vou começar a implementar a navegação condicional no SelectionFragment. Primeiro, obtenho uma instância de selectionViewModel para obter acesso ao armazenamento de dados. Em seguida, observo a seleção do usuário e a utilizo para restaurar o status da caixa de seleção. Para manter a seleção do usuário, atualizarei o estado quando a caixa de seleção for clicada chamando saveCoffeeTrackerSelection().

val selectionViewModel: SelectionViewModel by viewModels {
    SelectionViewModelFactory(
        UserPreferencesRepository.getInstance(requireContext())
    )
}

selectionViewModel.checkCoffeeTrackerEnabled().observe(
    viewLifecycleOwner
) { selection ->
    if (selection == UserPrefRepository.Selection.DONUT_AND_COFFEE){
        binding.checkBox.isChecked = true
    }
}

binding.button.setOnClickListener { button ->
    val coffeeSelected = binding.checkBox.isChecked                
    selectionViewModel.saveCoffeeTrackerSelection(coffeeSelected)
    //...

Agora é hora de atualizar as guias inferiores com a seleção do usuário. Se o usuário optar por desabilitar o rastreamento do café, a única opção deixada nas guias inferiores será a donutList, o que significa que podemos remover com segurança as guias inferiores. Em MainActivity, adiciono um observador e atualizo a visibilidade das guias inferiores. Para fazer isso, vou adicionar um observador e atualizar a visibilidade do BottomNavigation de acordo com a seleção do usuário.

private fun setupMenu(
    selection: UserPreferencesRepository.Selection
) {
    val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
    bottomNav.isVisible = when (selection) {
        UserPreferencesRepository.Selection.DONUT_AND_COFFEE -> true
        else -> false
    }
}

Em onCreate():

val selectionViewModel: SelectionViewModel by viewModels {
    SelectionViewModelFactory(
        UserPreferencesRepository.getInstance(this)
    )
}

selectionViewModel.checkCoffeeTrackerEnabled().observe(this) { s ->
    setupMenu(s)
}

Executando o aplicativo neste estado, você verá que ativar ou desativar o rastreador de café adiciona ou remove as guias inferiores do aplicativo. Isso é ótimo, mas não seria bom se enviássemos automaticamente o usuário para fazer uma seleção quando executasse o aplicativo pela primeira vez.

DonutList é o fragmento padrão e nosso destino inicial, o que significa que o aplicativo sempre começa com a DonutList, eu verifico se o usuário fez uma seleção anteriormente e aciono a navegação para SelectionFragment se não o fez.

donutListViewModel.isFirstRun().observe(viewLifecycleOwner) { s ->
    if (s == UserPreferencesRepository.Selection.NOT_SELECTED) {
        val navController = findNavController()
        navController.navigate(
            DonutListDirections.actionDonutListToSelectionFragment()
        )
    }
}

Antes de testar isso, desinstalo o aplicativo do dispositivo para ter certeza de que não há preferências salvas de minhas execuções anteriores. Agora, quando executo o aplicativo, ele me leva ao selectionFragment. Os lançamentos posteriores do aplicativo lembrarão da seleção que fiz e me levarão ao destino inicial correto.

E é isso! Adicionamos navegação condicional ao aplicativo rastreador de rosca. Mas como podemos testar esse fluxo? Excluir o aplicativo ou os dados do aplicativo todas as vezes antes de executar o teste não é o ideal. É aqui que o teste vem para resgatar!

 

Teste de navegação

Eu crio um novo teste chamado OneTimeFlowTest em androidTestFolder. Em seguida, crio um teste chamado testFirstRun() e anoto-o com @Test. Agora posso começar a implementar o teste. Primeiro, crio um TestNavHostController() usando o applicationContext. Também defino o nav_graph do aplicativo para a instância testNavigationController que acabei de criar.

@Test
fun testFirstRun() {
    // Create a mock NavController
    val mockNavController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )
    mockNavController.setGraph(R.navigation.nav_graph)
    //...
}

O mockNavigationController está pronto para uso. É hora de criar o cenário. Para fazer isso, eu inicio o aplicativo com o fragmento DonutList e defino a instância mockNavigationController que criei antes. Em seguida, verifique se o aplicativo navega automaticamente para o selectionFragment conforme o esperado.

val scenario = launchFragmentInContainer {
    DonutList().also { fragment ->
        fragment.viewLifecycleOwnerLiveData.observeForever{   
            viewLifecycleOwner ->
            if (viewLifecycleOwner != null){
                Navigation.setViewNavController(
                    fragment.requireView(), 
                    mockNavController
                )
            }
        }
    }
}

scenario.onFragment {
    assertThat(
        mockNavController.currentDestination?.id
    ).isEqualTo(R.id.selectionFragment)
}

 

Agora eu executo o teste e espero o resultado … e o teste passa com louvor! ou bem pode ser apenas verde.

 

Resumo

Neste artigo, adicionei navegação condicional ao aplicativo DonutTracker e também adicionei um teste para verificar se o fluxo funciona! Você pode verificar o código da solução aqui.

Com a navegação condicional, o aplicativo rastreador de rosca acionará o fluxo único para levar os usuários ao fragmento de seleção quando eles iniciarem o aplicativo pela primeira vez. Se o usuário optar por desabilitar o rastreador de café, o aplicativo remove a coffeeList do menu de navegação.

Com isso, a funcionalidade do rastreador de café está completa! Nos próximos artigos, aprenderemos como usar gráficos aninhados e modularizar este aplicativo.