Múltiplas pilhas traseiras
Se uma “pilha de retorno” é um conjunto de telas que você pode navegar de volta por meio do botão Voltar do sistema, “várias pilhas de retorno” são apenas um monte delas, certo? Bem, isso é exatamente o que fizemos com o suporte múltiplo back stack adicionado no Navigation 2.4.0-alpha01 e Fragment 1.4.0-alpha01!
Conteudo
As alegrias do botão de voltar do sistema
Esteja você usando o novo sistema de navegação por gestos do Android ou a barra de navegação tradicional, a capacidade dos usuários de ‘voltar’ é uma parte fundamental para a experiência do usuário no Android e fazer isso da maneira certa é uma parte importante para fazer seu aplicativo parecer um parte natural do ecossistema.
Nos casos mais simples, o botão Voltar do sistema apenas finaliza sua atividade. Embora no passado você pudesse se sentir tentado a substituir o método onBackPressed()
de sua atividade para personalizar esse comportamento, é 2021 e isso é totalmente desnecessário. Em vez disso, existem APIs para navegação personalizada de volta no OnBackPressedDispatcher
. Esta é na verdade a mesma API que o FragmentManager
e o NavController
já se conectam.
Isso significa que quando você usa Fragments ou Navigation, eles usam o OnBackPressedDispatcher
para garantir que, se você estiver usando as APIs de back stack, o botão Voltar do sistema funcionará para reverter cada uma das telas que você empurrou para a back stack.
Várias pilhas anteriores não mudam esses fundamentos. O botão de voltar do sistema ainda é um comando direcional – “voltar”. Isso tem um efeito profundo em como as várias APIs de back stack funcionam.
Múltiplas pilhas traseiras em fragmentos
No nível da superfície, o suporte para várias pilhas traseiras é enganosamente simples, mas requer um pouco de uma explicação do que realmente é a “pilha traseira de fragmentos”. A pilha de retorno do FragmentManager
não é composta de fragmentos, mas sim de transações de fragmentos. Especificamente, aqueles que usaram a API addToBackStack(String name)
.
Isso significa que quando você commit()
uma transação de fragmento com addToBackStack()
, o FragmentManager
vai executar a transação passando por e executando cada uma das operações (a substituição, etc.) que você especificou na transação, movendo cada fragmento até o estado esperado. FragmentManager
então mantém essa transação como parte de sua pilha de retorno.
Quando você chama popBackStack()
(diretamente ou por meio da integração do FragmentManager
com o botão Voltar do sistema), a transação superior na pilha de retorno do fragmento é revertida – um fragmento adicionado é removido, um fragmento oculto é mostrado, etc. Isso coloca o FragmentManager
de volta no mesmo estado em que estava antes da transação do fragmento ser inicialmente confirmada.
Nota: Eu não posso enfatizar isso o suficiente, mas você absolutamente nunca deve intercalar transações com
addToBackStack()
e transações sem no mesmoFragmentManager
: transações em sua pilha traseira felizmente não estão cientes de transações de fragmento de alteração de pilha não reversa – trocando coisas de baixo transações faz essa reversão quando você apresenta uma proposta muito mais arriscada.
Isso significa que popBackStack()
é uma operação destrutiva: qualquer fragmento adicionado terá seu estado destruído quando a transação for interrompida. Isso significa que você perde seu estado de visualização, qualquer estado de instância salvo e todas as instâncias de ViewModel
anexadas a esse fragmento são apagadas. Esta é a principal diferença entre essa API e o novo saveBackStack()
. saveBackStack()
faz a mesma reversão que popping a transação faz, mas garante que o estado de exibição, estado de instância salva e instâncias de ViewModel
sejam salvos da destruição. É assim que a API restoreBackStack()
pode posteriormente recriar essas transações e seus fragmentos do estado salvo e efetivamente ‘refazer’ tudo o que foi salvo. Magia!
No entanto, isso não aconteceu sem pagar muitas dívidas técnicas.
Pagando nossas dívidas técnicas em Fragments
Embora os fragmentos sempre salvaram o estado de exibição do Fragment, a única vez que o onSaveInstanceState()
de um fragmento seria chamado seria quando o onSaveInstanceState()
da Activity fosse chamado. Para garantir que o estado da instância salvo seja salvo ao chamar saveBackStack()
, precisamos também injetar uma chamada para onSaveInstanceState()
no ponto certo nas transições do ciclo de vida do fragmento. Não podemos chamá-lo muito cedo (seu fragmento nunca deve ter seu estado salvo enquanto ainda estiver STARTED
), mas não tarde demais (você deseja salvar o estado antes que o fragmento seja destruído).
Este requisito deu início a um processo para corrigir como FragmentManager
muda para estado para garantir que haja um local que gerencie a movimentação de um fragmento para seu estado esperado e lida com o comportamento de reentrada e todas as transições de estado que vão para os fragmentos.
Após 35 mudanças e 6 meses nessa reestruturação de fragmentos, descobriu-se que os fragmentos adiados foram seriamente quebrados, levando a um mundo onde as transações postergadas foram deixadas flutuando no limbo – não realmente confirmadas e não realmente não confirmadas. Mais de 65 mudanças e mais 5 meses depois, e reescrevemos completamente a maioria dos detalhes internos de como o FragmentManager
gerencia o estado, as transições adiadas e as animações.
O que esperar em fragmentos
Com a dívida técnica quitada (e um FragmentManager
muito mais confiável e compreensível), a ponta do iceberg APIs de saveBackStack()
e restoreBackStack()
foram adicionados.
Se você não usar essas novas APIs, nada muda: a única pilha de retorno do FragmentManager
funciona como antes. A API addToBackStack()
existente permanece inalterada – você pode usar um nome
nulo ou qualquer nome
que desejar. No entanto, esse nome
assume uma nova importância quando você começa a olhar para várias pilhas anteriores: é esse nome que é a chave única para aquela transação de fragmento que você usaria com saveBackStack()
e posteriormente com restoreBackStack().
Isso pode ser mais fácil de ver em um exemplo. Digamos que você tenha adicionado um fragmento inicial à sua atividade e, em seguida, feito duas transações, cada uma com uma única operação de replace
:
// Este é o fragmento inicial que o usuário vê fragmentManager.commit { setReorderingAllowed(true) replace<HomeFragment>(R.id.fragment_container) } // Posteriormente, em resposta às ações do usuário, adicionamos mais dois // transações para a pilha posterior fragmentManager.commit { setReorderingAllowed(true) replace<ProfileFragment>(R.id.fragment_container) addToBackStack(“profile”) } fragmentManager.commit { setReorderingAllowed(true) replace<EditProfileFragment>(R.id.fragment_container) addToBackStack(“edit_profile”) }
Isso significa que nosso FragmentManager
se parece com:
Digamos que queremos trocar nossa pilha de retorno de perfil e trocar para o fragmento de notificações. Chamaríamos saveBackStack()
seguido por uma nova transação:
fragmentManager.saveBackStack("profile") fragmentManager.commit { setReorderingAllowed(true) replace<NotificationsFragment>(R.id.fragment_container) addToBackStack("notifications") }
Agora nossa transação que adicionou o ProfileFragment
e a transação que adicionou o EditProfileFragment
foi salva no “profile
” chave. Esses fragmentos tiveram seu estado salvo completamente e FragmentManager
está mantendo seu estado junto com o estado da transação. Importante: essas instâncias de fragmento não existem mais na memória ou no FragmentManager
– é apenas o estado (e qualquer estado não configurado na forma de instâncias de ViewModel
):
A troca de volta é bastante simples: podemos fazer a mesma operação saveBackStack()
em nossa transação de “notifications
” e, em seguida, restoreBackStack()
:
fragmentManager.saveBackStack(“notifications”) fragmentManager.restoreBackStack(“profile”)
As duas pilhas trocaram de posição efetivamente:
Este estilo de manter uma única pilha de retorno ativa e trocar transações nela garante que o FragmentManager
e o resto do sistema sempre tenham uma visão consistente do que realmente deve acontecer quando o botão de retorno do sistema é pressionado. Na verdade, essa lógica permaneceu inteiramente inalterada: ela ainda apenas retira a última transação da pilha de volta do fragmento como antes.
Essas APIs são propositalmente mínimas, apesar de seus efeitos subjacentes. Isso torna possível construir sua própria estrutura em cima desses blocos de construção, evitando quaisquer hacks para salvar o estado de exibição Fragment, estado de instância salva e estado de não configuração.
Claro, se você não quiser construir sua própria estrutura em cima dessas APIs, você também pode usar a que fornecemos.
Trazendo várias pilhas de volta para qualquer tipo de tela com Navegação
O Navigation Component foi construído desde o início como um runtime genérico que não sabe nada sobre Views, Fragments, Composables, ou qualquer outro tipo de tela ou “destino” que você possa implementar em sua atividade. Em vez disso, é responsabilidade de uma implementação da interface NavHost
adicionar uma ou mais instâncias do Navigator
que saibam como interagir com um determinado tipo de destino.
Isso significa que a lógica para interagir com os fragmentos foi totalmente encapsulada no artefato navigation-fragment
e seus FragmentNavigator
e DialogFragmentNavigator
. Da mesma forma, a lógica para interagir com Composables está no artefato navigation-compose
completamente independente e seu ComposeNavigator
. Essa abstração significa que se você deseja construir seu aplicativo exclusivamente com Composables, você não é forçado a extrair qualquer dependência de fragmentos ao usar o Navigation Compose.
Este nível de separação significa que existem realmente duas camadas para várias pilhas posteriores na navegação:
- Salvar o estado das instâncias individuais de
NavBackStackEntry
que compõem a pilha de retorno doNavController
. Isso é responsabilidade doNavController
. - Salvar qualquer estado específico do
Navigator
associado a cadaNavBackStackEntry
(por exemplo, o fragmento associado a um destinoFragmentNavigator
). Isso é responsabilidade doNavigator
.
Atenção especial foi dada aos casos em que o Navigator
não foi atualizado para suportar o salvamento de seu estado. Enquanto a API Navigator
subjacente foi totalmente reescrita para suportar o estado de salvamento (com novas sobrecargas de suas APIs navigate()
e popBackStack()
que você deve substituir em vez das versões anteriores), NavController salvará o estado NavBackStackEntry
mesmo se o Navigator
não tiver sido atualizado (compatibilidade com versões anteriores é um grande negócio no mundo Jetpack!).
PS: esta nova API do
Navigator
também torna mais fácil testar seu próprioNavigator
de forma isolada, anexando umTestNavigatorState
que atua como ummini-NavController
.
Se você estiver usando apenas o Navigation
em seu aplicativo, o nível do Navigator é mais um detalhe de implementação do que algo com o qual você precisará interagir diretamente. Basta dizer que já fizemos o trabalho necessário para obter o FragmentNavigator
e o ComposeNavigator
para as novas APIs do Navigator
para que eles salvem e restaurem corretamente seu estado; não há trabalho que você precise fazer nesse nível.
Habilitando várias pilhas anteriores na navegação
Se você estiver usando NavigationUI
, nosso conjunto de auxiliares opinativos para conectar seu NavController
aos componentes de visualização de Material, você descobrirá que várias pilhas traseiras estão ativadas por padrão para itens de menu, BottomNavigationView
(e agora NavigationRailView!
) E NavigationView
. Isso significa que a combinação comum de usar o fragmento de navegação e interface de usuário de navegação simplesmente funcionará.
As APIs NavigationUI
são propositadamente construídas em cima das outras APIs públicas disponíveis no Navigation, garantindo que você possa construir suas próprias versões para precisamente o seu conjunto de componentes personalizados que você deseja. As APIs para permitir salvar e restaurar uma pilha de retorno não são exceção a isso, com novas APIs em NavOptions
, navOptions
Kotlin DSL, no XML de navegação e em uma sobrecarga para popBackStack()
que permite especificar que deseja uma operação pop para salvar o estado ou você deseja uma operação de navegação para restaurar algum estado salvo anteriormente.
Por exemplo, no Compose, qualquer padrão de navegação global (seja uma barra de navegação inferior, barra de navegação, gaveta ou qualquer coisa que você possa imaginar) pode usar a mesma técnica que mostramos para integração com BottomNavigation e chamar navegate()
com os atributos saveState
e restoreState
:
onClick = { navController.navigate(screen.route) { // Abra o destino inicial do gráfico para // evite acumular uma grande pilha de destinos // na pilha posterior conforme os usuários selecionam os itens popUpTo(navController.graph.findStartDestination().id) { saveState = true } // Evite várias cópias do mesmo destino quando // selecionando novamente o mesmo item launchSingleTop = true // Restaura o estado ao selecionar novamente um item previamente selecionado restoreState = true } }
Salve seu estado, salve seus usuários
Uma das coisas mais frustrantes para um usuário é perder seu estado. Essa é uma das razões pelas quais os fragmentos têm uma página inteira sobre o estado de salvamento e uma das muitas razões pelas quais estou tão feliz em ter cada camada atualizada para suportar várias pilhas antigas:
- Fragmentos (ou seja, sem usar o componente de navegação): esta é uma alteração opcional usando as novas APIs
FragmentManager
desaveBackStack
erestoreBackStack
. - O Navigation Runtime principal: adiciona novos métodos
NavOptions
opt-in pararestoreState
esaveState
e uma nova sobrecarga depopBackStack()
que também aceita um booleanosaveState
(o padrão é false). - Navegação com fragmentos: o
FragmentNavigator
agora utiliza as novas APIs doNavigator
para traduzir adequadamente as APIs do Navigation Runtime nas APIs do Fragment usando as APIs do Navigation Runtime. NavigationUI
:O onNavDestinationSelected()
,NavigationBarView.setupWithNavController()
eNavigationView.setupWithNavController()
agora usam as novasNavOptions
restoreState
esaveState
por padrão sempre que iriam abrir a pilha de retorno. Isso significa que todos os aplicativos que usam essas APIs NavigationUI obterão várias pilhas de retorno sem nenhuma alteração de código de sua parte após a atualização do Navigation 2.4.0-alpha01 ou superior.
Se você quiser ver mais alguns exemplos que usam essa API, dê uma olhada em NavigationAdvancedSample
(recentemente atualizado sem nenhum código de NavigationExtensions
que costumava exigir para suportar várias pilhas antigas):
E para Navigation Compose, considere olhar para Tivi:
Se você encontrar algum problema, certifique-se de usar o rastreador de problemas oficial para registrar bugs em Fragments ou Navigation e nós iremos dar uma olhada neles!