Dia 9 — Navegação entre telas
React Navigation e o Navigator
React Navigation e o Navigator
Trocar de tela sem recarregar o app é o que a navegação resolve. A peça
principal é a pilha: cada tela que abre fica por cima da anterior, e voltar
é retirar a última. O pacote que implementa isso no React Native atual é o
React Navigation, e o componente que cria a pilha vem do módulo de pilha nativa
através de createNativeStackNavigator.
A montagem tem três peças e a ordem importa:
- O
NavigationContainer— o contêiner que segura o estado da navegação. - O
Stack.Navigator— declara que tipo de navegação é, pilha ou aba. - O
Stack.Screen— uma linha por tela, comnamee o componente.
const Stack = createNativeStackNavigator(); function Rotas() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Pedidos" component={TelaPedidos} /> <Stack.Screen name="Detalhe" component={TelaDetalhe} /> </Stack.Navigator> </NavigationContainer> ); }
O Stack.Screen é só declaração: ele não é a tela. name é a chave usada
para navegar e o que aparece no cabeçalho por padrão; component é a função que
devolve o conteúdo. Isso explica um comportamento que confunde: uma tela que
define options recebe o objeto { navigation, route } nas props, e o mesmo
acesso por hook funciona em qualquer ponto abaixo do contêiner.
O exemplo desta página isola a linha de declaração em um array e imprime o que
ela carrega: os três name — Pedidos, Detalhe, Editar —, o primeiro como
primeira tela da pilha, e o options da primeira como `{ title: 'Meus pedidos'
}. Uma tela sem options não tem título próprio no objeto: o name` é o que
vira título, e é por isso que a segunda e a terceira linhas do array não trazem
nada além do name e do component.
navegar e voltar
navigation.navigate('Detalhe') empilha; navigation.goBack() desempilha. A
diferença que importa: navigate para uma tela que já está na pilha volta
para ela em vez de duplicar, e é por isso que ele é o método certo para menu.
push, que empilha sempre, é o certo para uma tela que pode abrir várias vezes,
como uma conversa de chat — onde voltar deve desfazer uma instância, não todas.
A diferença aparece inteira na saída do exemplo. A primeira navegação empilha
Detalhe e a profundidade vai para 2; a segunda empilha Editar e a
profundidade vai para 3. Os dois goBack devolvem a pilha ao array `[ 'Pedidos'
]` — o que prova que voltar é retirar a última e não sair da tela. E o terceiro
goBack não falha: devolve "nao ha tela anterior", porque a pilha já está no
começo. Uma pilha de tamanho 1 não tem o que desempilhar.
O par navigate e empilhar resolve a diferença do parágrafo de cima com um
número: navegar para Pedidos quando ele já está na pilha devolve a
profundidade 3, sem duplicar; empilhar a mesma tela devolve 4. A primeira
volta, a segunda empilha. É a mesma tela e o mesmo nome — o que muda é o método,
e é o método que decide se voltar desfaz uma instância ou todas.
navigation.setOptions muda o cabeçalho da tela atual sem declarar outra
tela. options na declaração serve para o padrão, e o setOptions serve para
o que depende do dado da tela: título com o nome do item, botão de editar que
só existe naquele registro. É a propriedade que resolve o "o cabeçalho precisa
saber o que a tela carregou".
O exemplo chama cabecalhoDe duas vezes e o objeto options muda junto: com
podeEditar: true o resultado é { title: 'ana', headerRight: 'editar' }; com
podeEditar: false é { title: 'bruno', headerRight: null }. O mesmo cabeçalho,
duas telas, e o botão de editar existe em uma e não na outra — sem declarar
nenhuma tela nova.
onde a pilha é montada
O contêiner de navegação fica no ponto de entrada do aplicativo, e as telas
ficam em arquivos separados. O erro de estrutura que trava a navegação não é de
sintaxe: é o contêiner montado dentro de uma tela. Assim a pilha é recriada
quando a tela monta, o histórico se perde ao voltar, e o comportamento muda
conforme de onde se chegou. A pilha tem um dono, e esse dono é o componente
raiz do aplicativo.
O JSON.stringify do exemplo mostra essa estrutura na ordem que importa: o
contêiner primeiro, e dentroDele com os nomes das três telas. O array de telas
é filho do contêiner — não de nenhuma tela. Inverter a ordem não é um detalhe
de leitura: é o que faz o goBack do parágrafo anterior perder o histórico.
A tela, do lado dela, é uma função que recebe { navigation, route } e devolve
conteúdo. O exemplo monta TelaDetalhe com as duas props e imprime o nome da
rota — Detalhe — que é o texto que a tela renderiza. route traz nome e
parâmetros, navigation traz o navigate e o goBack, e nenhum dos dois é
passado por outra tela: chegam na prop porque estão no contêiner.
Exemplo
// A navegacao e uma PILHA: `navigate` empilha, `goBack` desempilha, e // `navigate` para uma tela ja aberta volta para ela em vez de duplicar. // O exemplo monta o array de telas e simula a pilha. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ tela: { padding: 16 } }); // A linha de declaracao de tela e so isso: `name` e a chave, `component` e // a funcao que devolve o conteudo. const telas = [ { name: 'Pedidos', component: 'TelaPedidos', options: { title: 'Meus pedidos' } }, { name: 'Detalhe', component: 'TelaDetalhe' }, { name: 'Editar', component: 'TelaEditar' }, ]; console.log('telas declaradas:', telas.map((t) => t.name)); console.log('a primeira tela da pilha:', telas[0].name); console.log('`options` serve para o padrao do cabecalho:', telas[0].options); console.log('sem `options`, o `name` vira o titulo'); // A pilha e um array: `navigate` empilha, `goBack` desempilha. function criarNavegacao(inicial) { const pilha = [inicial]; return { navegar(para) { if (pilha[pilha.length - 1] === para) return 'ja estava na tela ' + para; pilha.push(para); return 'abriu ' + para; }, voltar() { if (pilha.length <= 1) return 'nao ha tela anterior'; const saiu = pilha.pop(); return 'voltou de ' + saiu + ' para ' + pilha[pilha.length - 1]; }, empilhar(para) { pilha.push(para); return 'empilhou ' + para; }, topo() { return pilha[pilha.length - 1]; }, profundidade() { return pilha.length; }, historico() { return pilha.slice(); }, }; } const nav = criarNavegacao('Pedidos'); console.log(nav.navegar('Detalhe')); console.log('topo da pilha:', nav.topo(), '| profundidade:', nav.profundidade()); console.log(nav.navegar('Editar')); console.log('topo da pilha:', nav.topo(), '| profundidade:', nav.profundidade()); console.log(nav.voltar()); console.log(nav.voltar()); console.log('a pilha voltou ao inicio:', nav.historico()); console.log(nav.voltar()); // `navigate` para tela ja aberta volta em vez de duplicar; `push` // empilharia sempre. const nav2 = criarNavegacao('Pedidos'); console.log(nav2.navegar('Detalhe')); console.log(nav2.navegar('Pedidos'), '| profundidade:', nav2.profundidade(), '-> nao duplicou'); console.log(nav2.empilhar('Pedidos'), '| profundidade:', nav2.profundidade(), '-> `push` empilha sempre'); // `setOptions` muda o cabecalho da tela atual com o dado que ela carregou. let opcoes = {}; function cabecalhoDe(dados) { opcoes = Object.assign({}, opcoes, { title: dados.cliente, headerRight: dados.podeEditar ? 'editar' : null, }); return 'cabecalho: ' + dados.cliente; } console.log(cabecalhoDe({ cliente: 'ana', podeEditar: true })); console.log('opcoes da tela atual:', opcoes); console.log(cabecalhoDe({ cliente: 'bruno', podeEditar: false })); console.log('com `setOptions`, o cabecalho muda conforme o dado:', opcoes); // A arvore de uma tela: ela recebe `navigation` e `route`. function TelaDetalhe(props) { return <Text style={estilos.tela}>{props.route.name}</Text>; } const arvore = TelaDetalhe({ navigation: nav, route: { name: 'Detalhe', params: { id: 'p41' } } }); console.log('tipo da tela:', arvore.type, '| nome na rota:', arvore.props.children); console.log('`route` traz nome e parametros; `navigation` traz o navigate e o goBack'); // A pilha tem UM dono: o container fica na entrada do aplicativo, e nunca // dentro de uma tela. const entradaDoApp = { container: 'contêiner de navegação', dentroDele: telas.map((t) => t.name) }; console.log('estrutura da entrada:', JSON.stringify(entradaDoApp)); console.log('container dentro de uma tela faz voltar perder o historico');
Saída real
telas declaradas: [ 'Pedidos', 'Detalhe', 'Editar' ]
a primeira tela da pilha: Pedidos
`options` serve para o padrao do cabecalho: { title: 'Meus pedidos' }
sem `options`, o `name` vira o titulo
abriu Detalhe
topo da pilha: Detalhe | profundidade: 2
abriu Editar
topo da pilha: Editar | profundidade: 3
voltou de Editar para Detalhe
voltou de Detalhe para Pedidos
a pilha voltou ao inicio: [ 'Pedidos' ]
nao ha tela anterior
abriu Detalhe
abriu Pedidos | profundidade: 3 -> nao duplicou
empilhou Pedidos | profundidade: 4 -> `push` empilha sempre
cabecalho: ana
opcoes da tela atual: { title: 'ana', headerRight: 'editar' }
cabecalho: bruno
com `setOptions`, o cabecalho muda conforme o dado: { title: 'bruno', headerRight: null }
tipo da tela: Text | nome na rota: Detalhe
`route` traz nome e parametros; `navigation` traz o navigate e o goBack
estrutura da entrada: {"container":"contêiner de navegação","dentroDele":["Pedidos","Detalhe","Editar"]}
container dentro de uma tela faz voltar perder o historico
Parâmetros entre telas e abas
Parâmetros entre telas e abas
Parâmetro de rota é o dado que uma tela entrega à próxima, e ele vai em
navigate no segundo argumento. O caminho completo é declaração, envio e
leitura, e cada parte tem o nome que importa:
// envio navigation.navigate('Detalhe', { id: item.id }); // leitura, dentro da tela de destino const { id } = route.params;
useRoute e useNavigation são os hooks que entregam esses dois objetos em
qualquer ponto da árvore, e useNavigation devolve também goBack,
setOptions, reset e push. São eles que resolvem o problema do repasse de
propriedades em navegação: passar navigation de mão em mão por três
componentes intermediários não é necessário, porque o hook alcança o contêiner
de onde a tela está.
O exemplo desta página percorre o caminho inteiro, e o console do envio mostra
a forma exata: navigate(Detalhe, {"id":"p41"}). O nome da tela vem primeiro, o
objeto de parâmetro vem segundo, e o objeto tem uma chave só. É esse texto que
vira o segundo argumento da chamada real.
O erro que trava a tela em branco é sempre o mesmo: route.params lido quando
params é undefined. Acontece quando a tela é aberta por outro caminho que
não passou parâmetro — notificação, restauração de sessão, link direto. A
alternativa segura é valor padrão: const id = route.params && route.params.id,
e tratar o ausente como estado vazio em vez de deixar a leitura estourar.
Os três console de rota do exemplo mostram exatamente esse problema e a
solução lado a lado. Com {"name":"Detalhe","params":{"id":"p41"}} a tela
mostra o pedido p41; com params undefined e com a chave params
inteiramente ausente, a tela mostra "pedido nao informado" — e não quebra. São
três caminhos de entrada e uma tela só, e a leitura protegida é o que faz os
dois últimos existirem.
o que parâmetro não deve ser
Parâmetro de rota é para o identificador, não para o registro inteiro.
Passar o objeto completo funciona e quebra depois: o que ficou guardado na rota
é uma cópia do momento da navegação, e se o registro mudar no servidor a tela
continua mostrando a cópia velha, sem nenhuma requisição para detectar. Passar
id obriga a tela a buscar, e é o que garante que o que aparece é o estado
atual.
O exemplo põe as duas formas lado a lado, e a diferença de tamanho é o argumento
inteiro: mandando só o id o objeto sai como {"id":"p41"}; mandando o
registro inteiro ele sai com cliente e total junto. A cópia velha é esse
segundo objeto — ele viaja inteiro pela navegação e chega na tela de destino
com os valores que tinha no momento do toque. Nada ali pede o estado atual ao
servidor.
A exceção que confirma a regra: passar o que é caro de buscar e que a tela não
precisa atualizar — o nome do cliente, para o cabeçalho aparecer sem esperar a
rede. É um cache deliberado de uma tela, não um modelo de dados.
abas
O construtor de abas troca a pilha por uma barra, e a montagem é a mesma forma
com outro construtor: contêiner, Tab.Navigator, Tab.Screen. A diferença que
importa está em dois detalhes. Abas mantêm estado: o componente monta uma vez e
a troca de aba não desmonta, então o filtro digitado na aba de lista continua
lá quando se volta. E tabBarIcon recebe o foco da aba, o que permite trocar
o ícone conforme ela está ativa.
O exemplo descreve cada aba pelo que ela contém por dentro, e a distinção fica
na string: "Pedidos" tem a pilha ListaPedidos > DetalhePedido, e "Perfil" tem
só DadosPerfil. São duas pilhas independentes, e é por isso que voltar dentro
da aba de pedidos não troca de aba — o goBack opera na pilha interna, que é a
que pertence à aba ativa.
A combinação que aparece em quase todo aplicativo é pilha por baixo e abas por
dentro: cada aba tem sua própria pilha, então voltar dentro da aba de pedidos
não leva para a aba de perfil. É um Navigator dentro de um Screen, e a
ordem importa — a aba é o contêiner de fora.
A manutenção de estado aparece no fim da saída: as três chamadas de
TrocaDeAba registram Pedidos > Perfil > Pedidos, e a aba de pedidos aparece
2 vezes no registro de montagem — o que a linha final traduz como "o estado
continua". Voltar a uma aba não remonta a tela; devolve a tela que ainda estava
montada, com o filtro digitado e a rolagem no lugar. É a diferença entre a aba
e o goBack: o goBack desempilha, a troca de aba não.
Exemplo
// Parametro de rota e o IDENTIFICADOR que uma tela entrega a outra, e // ele vai no segundo argumento da navegacao. O exemplo mostra o envio, // a leitura e o que acontece quando o parametro nao vem. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ linha: { padding: 12 } }); // (1) envio: o segundo argumento do `navigate` leva o parametro. function ItemPedido(props) { return ( <View style={estilos.linha}> <Text onPress={() => props.navigation.navigate('Detalhe', { id: props.pedido.id })}> {props.pedido.cliente} </Text> </View> ); } // (2) leitura, na tela de destino. O `params` pode nao vir, e o exemplo // trata o ausente em vez de deixar a leitura estourar. function TelaDetalhe(props) { const rota = props.route; const params = rota.params; const id = params && params.id ? params.id : null; if (id === null) { return <Text style={estilos.linha}>pedido nao informado</Text>; } return <Text style={estilos.linha}>pedido {id}</Text>; } const pedido = { id: 'p41', cliente: 'ana', total: 90 }; const lista = ItemPedido({ pedido, navigation: { navigate: () => {} } }); console.log('item montado:', lista.type, '| texto:', lista.props.children.props.children); // O envio: e o `navigate` com o parametro. function enviar(destino, parametros) { return 'navigate(' + destino + ', ' + JSON.stringify(parametros) + ')'; } console.log('envio:', enviar('Detalhe', { id: pedido.id })); // (3) `route.params` ausente e o caminho seguro de leitura. const rotaCompleta = { name: 'Detalhe', params: { id: 'p41' } }; const rotaVazia = { name: 'Detalhe', params: undefined }; const rotaDeNotificacao = { name: 'Detalhe' }; for (const rota of [rotaCompleta, rotaVazia, rotaDeNotificacao]) { const arvore = TelaDetalhe({ route: rota }); console.log('rota', JSON.stringify(rota), '-> tela:', arvore.props.children); } // (4) o hook entrega `route` e `navigation` em qualquer ponto da arvore, // sem passar de mao em mao. function dentroDeUmComponente() { const params = rotaCompleta.params; return params.id; } console.log('o mesmo dado, lido de dentro do componente:', dentroDeUmComponente()); console.log('metodos de `navigation`: navigate, goBack, push, reset, setOptions'); // (5) parametro de rota e IDENTIFICADOR, nao o registro inteiro. console.log('mandando so o id:', JSON.stringify({ id: pedido.id })); console.log('mandando o registro inteiro:', JSON.stringify(pedido), '-> uma copia velha da tela'); console.log('quem recebe o id precisa buscar, e assim mostra o estado atual'); // (6) abas: cada aba e um `Navigator` com pilha propria. const abas = [ { name: 'Pedidos', pilha: ['ListaPedidos', 'DetalhePedido'] }, { name: 'Perfil', pilha: ['DadosPerfil'] }, ]; for (const aba of abas) { console.log('aba', aba.name, '| pilha interna:', aba.pilha.join(' > ')); } console.log('voltar dentro da aba de pedidos nao troca de aba'); console.log('a aba e o container de fora; a pilha e a de dentro'); // (7) aba mantem estado: trocar de aba nao remonta a tela. const visitadas = []; function TrocaDeAba(props) { visitadas.push(props.aba); return <Text>{props.aba}</Text>; } TrocaDeAba({ aba: 'Pedidos' }); TrocaDeAba({ aba: 'Perfil' }); TrocaDeAba({ aba: 'Pedidos' }); console.log('ordem das visitas:', visitadas.join(' > ')); console.log('a aba de pedidos foi montada', visitadas.filter((a) => a === 'Pedidos').length, 'vezes, e o estado continua');
Saída real
item montado: View | texto: ana
envio: navigate(Detalhe, {"id":"p41"})
rota {"name":"Detalhe","params":{"id":"p41"}} -> tela: [ 'pedido ', 'p41' ]
rota {"name":"Detalhe"} -> tela: pedido nao informado
rota {"name":"Detalhe"} -> tela: pedido nao informado
o mesmo dado, lido de dentro do componente: p41
metodos de `navigation`: navigate, goBack, push, reset, setOptions
mandando so o id: {"id":"p41"}
mandando o registro inteiro: {"id":"p41","cliente":"ana","total":90} -> uma copia velha da tela
quem recebe o id precisa buscar, e assim mostra o estado atual
aba Pedidos | pilha interna: ListaPedidos > DetalhePedido
aba Perfil | pilha interna: DadosPerfil
voltar dentro da aba de pedidos nao troca de aba
a aba e o container de fora; a pilha e a de dentro
ordem das visitas: Pedidos > Perfil > Pedidos
a aba de pedidos foi montada 2 vezes, e o estado continua