Flutter: Push, Pop, Push
Na Flutter Portugal acreditamos que o conhecimento tem de ser acessível a todos, independentemente do grau de conhecimento de línguas estrangeiras. Assim, vamos lançar uma pequena série de artigos que vão abrangir vários tópicos: Dados persistidos, State Management, Navegação, etc…
Este artigo foi originalmente escrito por Pooja Bhaumik com o título “Flutter: Push, Pop, Push” e traduzido com autorização da autora.
Construir uma UI (Interface do Utilizador) é muito simples com todos os widgets que a framework disponibiliza, parte da qual abordei no meu último artigo. Mas não podemos ter somente uma aplicação linda que não faz nada funcional. Ser-nos-à pedido que naveguemos pela aplicação ou que enviemos dados para a frente e para trás entre ecrãs. Em Flutter, a navegação de um ecrã para outro é possível devido aos Navigators, um widget simples que mantém uma stack (pilha) de Routes (rotas), ou em termos mais simples, um histórico de páginas/ecrãs visitados.
Encontraremos imensos artigos que dizem como push (empurrar)para um novo ecrã, ou pop (remover) do ecrã atual, mas este artigo é um pouco mais do que isso. Este irá focar-se mais na maioria dos métodos do Navigator e descrever um caso prático para cada um deles.
Antes de começarmos…
Mencionou Routes algures, o que é isso?
Routes é uma abstração para um ecrã ou página de uma aplicação. Por exemplo, '/home'
vai levar-nos ao HomeScreen (Ecrã Inicial) ou '/login'
vai levar-nos ao LoginScreen (Ecrã de Login). '/'
vai ser a nossa rota inicial. Isto pode soar parecido ao Routing no desenvolvimento REST API. Então '/'
pode atuar como uma root (raiz).
Esta é a forma como iríamos declarar as nossas rotas na nossa aplicação Flutter.
new MaterialApp(
home: new Screen(),
routes: <String, WidgetBuilder> {
'/screen1': (BuilderContext context) => new Screen1(),
'/screen2' : (BuilderContext context) => new Screen2(),
'/screen3' : (BuilderContext context) => new Screen3(),
'/screen4' : (BuilderContext context) => new Screen4()
},
)
Screen1(), Screen2(), etc são os nomes das classes para cada ecrã.
Push, push, push.
Se tem algum conhecimento sobre Data Structures (Estruturas de Dados), então sabe sobre Stacks. Se tem conhecimentos, ainda que básicos, sobre stacks, então sabe sobre push e pop.
Se não sabe, pushing é adicionar um elemento ao topo da stack de elementos e popping é remover o elemento no topo da mesma stack.
No caso do Flutter, quando navegamos para outro ecrã, usamos o método push
e o widget Navigator
adiciona o novo ecrã ao topo da stack. Naturalmente, o método pop
iria remover esse ecrã da stack.
Avancemos então para a base de código do nosso projeto-amostra e vejamos como é que podemos movimentar-nos do Ecrã 1 para o Ecrã 2. Podemos experimentar com os métodos ao correr a aplicação-amostra.
new RaisedButton(
onPressed:(){
Navigator.of(context).pushNamed('/screen2');
},
child: new Text("Push to Screen 2"),
),
Isto foi curto.
Lá isso foi. Com a ajuda dos métodos pushNamed
, podemos navegar para qualquer ecrã cuja rota é definida em main.dart
. Chamamo-lhes namedRoute
para referência. O caso prático deste método é bastante direto. Para simplesmente navegar.
Pop it
Agora, quando nos queremos ver livres do último ecrã visitado, que é o Screen2
neste caso, precisaríamos de remover Routes da stack do Navigator usando o método pop
.
Navigator.of(context).pop();
Não esquecer que esta linha de código vai por dentro do método OnPressed
.
Quando se usa uma Scaffold, normalmente não é necessário fazer pop da rota explicitamente, porque o Scaffold automaticamente adiciona um botão “back” (voltar) na sua AppBar, o qual irá chamar o Navigator.pop()
quando pressionado. Mesmo em Android, pressionar o botão “back” do dispositivo faria o mesmo. Mas mesmo assim, poderemos precisar deste método para outros casos práticos, tais como pop de um AlertDialog quando o utilizador clica no botão de Cancelar.
Porquê pop em vez de fazer push de volta ao ecrã anterior?
Imaginemos que temos uma aplicação de Reserva de Hotel que lista os hotéis na localização que desejamos. Clicar numa das listas vai levar-nos a um ecrã que contém mais detalhes sobre o hotel. Escolhemos um, mas odiamos o hotel e queremos voltar à lista. Se fizermos push
de volta ao HotelListScreen
, vamos estar também a manter o nosso DetailsScreen
na nossa stack. Assim, clicar no botão de voltar atrás levar-nos-ia de novo ao DetailsScreen
. Tão confuso!
Em vez disso devemos testá-lo. Correr a aplicação-amostra, ver a appBar no nosso Screen1
, não tem nenhum botão para voltar atrás, porque é a Route inicial (ou ecrã inicial), agora clicamos em Push to Screen 2 e, em vez do botão de retornar, carregar no Push to Screen 1 em vez de Pop e, agora, ver a appBar no Screen1
.
Carregar no recente botão de voltar atrás vai fazer-nos recuar ao Screen2
e nós não queremos isso neste tipo de casos.
maybePop
E se estivermos na Route inicial e alguém, por engano, tentou fazer pop a este ecrã. Popping o único ecrã na stack iria fechar a nossa aplicação, porque assim não teria nenhuma rota para exibir. Nós definitivamente não queremos que o utilizador tenha uma experiência tão inesperada. É aí que o maybePop()
aparece. Por isso, fica do género “faça pop só se conseguir”. Experimente, clique no botão maybePop
no Screen1
e não vai fazer nada. Porque não há nada para fazer pop. Agora tente o mesmo no Screen3
e irá fazer pop ao ecrã. Porque pode.
canPop
É espantoso conseguirmos fazer isto, mas como é que posso saber se isto é a route inicial? Seria bom se pudesse exibir algum alerta para o utilizador em casos destes.
Ótima questão, é só chamar este método canPop()
e regressará true
se esta route puder ser popped e false
se não for possível.
Esperimente ambos os métodos canPop
e maybePop
no Screen1
e Screen3
, e veja a diferença. Os print values para o canPop
vão mostrar no separador da sua consola/terminal do seu IDE (Ambiente de Desenvolvimento Integrado).
Push um pouco mais
Voltemos a mais métodos de push. Agora estamos a aprofundar. Vamos falar sobre substituir uma rota por uma nova rota. Temos dois métodos que podem fazer isto — pushReplacementNamed
e popAndPushNamed
.
Navigator.of(context).pushReplacementNamed('/screen4');
//and
Navigator.popAndPushNamed(context, '/screen4');
Tente experimentar com ambos no Screen3
da aplicação-amostra. E repare na animação de saída e entrada em cada caso. pushReplacementNamed
vai executar a animação de entrada e popAndPushNamed
vai executar a animação de saída. Podemos utilizar isto para os próximos casos práticos possíveis.
Caso prático: pushReplacementNamed
Quando o utilizador iniciou sessão com sucesso, e agora poderá estar no DashboardScreen
, não queremos que o utilizador regresse ao LoginScreen
em caso algum. Então, a rota de login deveria estar completamente substituída pela rota do painel. Outro exemplo seria ir ao HomeScreen
a partir do SplashScreen
. Só deverá ser mostrado uma vez e o utilizador não deveria ser capaz de regressar a ele novamente a partir do HomeScreen
. Em casos destes, uma vez que vamos passar para um ecrã completamente novo, poderemos querer utilizar este método para a sua propriedade de animação de entrada.
Caso prático: popAndPushNamed
Suponhamos que estamos a construir uma aplicação de Compras que mostra uma lista de produtos no seu ProductsListScreen
e o utilizador pode aplicar filtros no FiltersScreen
. Quando o utilizador clica no botão Apply Changes, o FiltersScreen deveria fazer pop e push de volta ao ProductsListScreen
com os novos valores do filtro. Aqui, a propriedade de animação de saída do popAndPushNamed
seria mais apropriada.
Até ao fim…
Estamos quase no fim do artigo. Bem, quase.
Nesta secção vamos abordar os próximos três métodos pushNameAndRemoveUntil
e popUntil
.
Caso prático: pushNamedAndRemoveUntil
Então, basicamente estamos a construir uma aplicação do género do Facebook/Instagram, em que o utilizador inicia sessão, percorre o seu feed, persegue perfis diferentes e, quando acaba, quer terminar a sessão na aplicação. Depois de terminar sessão, não podemos simplesmente fazer push
num HomeScreen
(ou em qualquer ecrã que precise de ser exibido depois de terminar a sessão) em casos deste tipo. Queremos remover todas as rotas na stack para que o utilizador não consiga regressar às rotas anteriores depois de ter terminado a sua sessão.
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
Aqui, (Route<dynamic> route => false
vai certificar-se de que todas as rotas anteriores à rota que foi pushed são removidas.
Agora, em vez de remover todas as rotas antes das rotas pushed, só podemos remover um certo número de rotas. Tomemos como exemplo outra aplicação de Compras! Ou basicamente qualquer aplicação que exija uma transação de pagamento.
Nestas aplicações, uma vez que o utilizador tenha completado a transação de pagamento, todos os ecrãs relacionados com a transação ou com o carrinho devem ser removidos da stack e o utilizador deve ser levado ao PaymentConfirmationScreen
. Clicar no botão de retroceder deverá levá-los de volta somente ao ProductsListScreen
ou HomeScreen
.
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
De acordo com o fragmento do código, fazemos push ao Screen4
e removemos todas as rotas até ao Screen1
para que a nossa stack se pareça com isto.
Caso prático: popUntil
Imaginemos que estamos a construir uma aplicação tipo Formulários Google ou uma aplicação que nos deixa preencher e organizar formulários Google. Um utilizador poderá ter de preencher um longo formulário de 3 partes, o qual poderá ser exibido em 3 ecrãs sequenciais numa aplicação móvel. Na 3ª parte do formulário, o utilizador decide cancelar o preenchimento do formulário. O utilizador clica em Cancel
e todos os ecrãs anteriores relacionados com o formulário devem ser popped e o utilizador deve ser levado de volta ao HomeScreen
ou DashboardScreen
,perdendo, assim, todos os dados relacionados com o formulário (que é o que desejamos neste tipo de casos). Não estaremos a fazer push em nada de novo aqui, só a retornar a uma rota anterior.
Navigator.popUntil(context, ModalRoute.withName('/screen2'));
Onde estão os dados?
Na maioria dos exemplos anteriores, estou apenas a fazer push numa nova rota sem enviar dados nenhuns, mas um cenário assim é muito pouco provável numa aplicação real. Para enviar dados, estaríamos a utilizar o Navigator para fazer push numa nova MaterialPageRoute para a stack com os nossos dados, (aqui é userName
).
String userName = "John Doe";
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new Screen5(userName)));
Para recuperar os valores no Screen5
, adicionaríamos um construtor parametrizado no Screen5
, desta forma:
class Screen5 extends StatelessWidget {
final String userName;
Screen5(this.userName);
@override
Widget build(BuildContext context) {
print(userName)
...
}
}
Isto significa que não só podemos usar o método MaterialPageRoute
para push
,mas também para pushReplacement
, pushAndPopUntil
, etc. Basicamente, introduza o termo-chave named
a partir dos métodos acima descritos e o primeiro parâmetro irá agora tomar MaterialPageRoute
em vez de um String
da namedRoute.
Devolve-me alguns dados, meu
Poderemos também querer devolver dados a partir de um novo ecrã. Suponhamos que estamos a construir uma aplicação de Alarme e, para definir um novo toque para o nosso alarme, iríamos exibir uma lista de opções de áudio. Obviamente, iríamos precisar do item de dados selecionado quando a caixa de Diálogo tiver sido popped. Pode ser conseguido assim:
new RaisedButton(onPressed: ()async{
String value = await Navigator.push(context, new MaterialPageRoute<String>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text('OK'),
onTap: () { Navigator.pop(context, "Audio1"); }
),
);
}
)
);
print(value);
},
child: new Text("Return"),)
Experimente no Screen4
e verifique a consola para os print values.
Também é de notar: Quando uma rota é usada para recuperar um valor, o tipo de parâmetro da rota deve corresponder ao tipo de resultados do pop. Aqui, necessitamos de dados de String (corda), por isso usámos MaterialPageRoute<String>
. Também não faz mal se não o tipo não for especificado.
Uau, isso foi muita informação
De facto, foi, e eu podia ter explicado somente os métodos e a sua implementação, mas visto que há tantos métodos Navigator, eu queria explicá-los usando cenários trazidos de aplicações do mundo real. Espero que isto vos tenha ajudado a alargar o vosso horizonte Navigator.