Flutter: Push, Pop, Push

Mariana Castanheira
Flutter Portugal
Published in
9 min readSep 24, 2019

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 pushe o widget Navigatoradiciona o novo ecrã ao topo da stack. Naturalmente, o método popiria 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 namedRoutepara referência. O caso prático deste método é bastante direto. Para simplesmente navegar.

Stack depois de fazer push no Screen2

Pop it

Agora, quando nos queremos ver livres do último ecrã visitado, que é o Screen2neste 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.

Stack depois de fazer pop ao Screen2

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 pushde volta ao HotelListScreen, vamos estar também a manter o nosso DetailsScreenna 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 Screen2e nós não queremos isso neste tipo de casos.

Esta vai ser a Stack numa situação do tipo

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 maybePopno Screen1e não vai fazer nada. Porque não há nada para fazer pop. Agora tente o mesmo no Screen3e 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á truese esta route puder ser popped e falsese não for possível.

Esperimente ambos os métodos canPope maybePopno Screen1e Screen3, e veja a diferença. Os print values para o canPopvã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');
Stack antes e depois

Tente experimentar com ambos no Screen3da aplicação-amostra. E repare na animação de saída e entrada em cada caso. pushReplacementNamedvai executar a animação de entrada e popAndPushNamedvai 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 LoginScreenem caso algum. Então, a rota de login deveria estar completamente substituída pela rota do painel. Outro exemplo seria ir ao HomeScreena 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 ProductsListScreene 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 ProductsListScreencom os novos valores do filtro. Aqui, a propriedade de animação de saída do popAndPushNamedseria 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 pushNameAndRemoveUntile 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 pushnum 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 => falsevai certificar-se de que todas as rotas anteriores à rota que foi pushed são removidas.

Terminar sessão remove todas as routes e leva o utilizador de volta ao LoginScreen

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 ProductsListScreenou HomeScreen.

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));

De acordo com o fragmento do código, fazemos push ao Screen4e removemos todas as rotas até ao Screen1para que a nossa stack se pareça com isto.

Antes e depois

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 Cancele todos os ecrãs anteriores relacionados com o formulário devem ser popped e o utilizador deve ser levado de volta ao HomeScreenou 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.

É feito pop em todos os ecrãs relacionados com Form
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 MaterialPageRoutepara push,mas também para pushReplacement, pushAndPopUntil, etc. Basicamente, introduza o termo-chave nameda partir dos métodos acima descritos e o primeiro parâmetro irá agora tomar MaterialPageRouteem vez de um Stringda 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 Screen4e 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.

--

--

Mariana Castanheira
Flutter Portugal

Translator (English-Portuguese/Portuguese-English). Helping Flutter Portugal for the time being.