Flutter - Como navegar entre telas com Nuvigator

Flutter - Como navegar entre telas com Nuvigator
Leonardo Marinho
Leonardo Marinho

Compartilhe

Navegação é um tema bastante presente tanto no universo de dispositivos móveis quanto em sites e demais aplicações que fornecem aos usuários a opção de mudar de tela. Neste artigo, aprenderemos como navegar entre telas com Flutter através do Nuvigator, que é uma implementação da Nubank para abstrair uma série de complexidades provenientes do navegador padrão que vem no Flutter. Bora aprender como ele funciona?

Antes de entrarmos no tópico sobre o Nuvigator propriamente dito, precisamos entender o que temos disponível hoje nativamente no Flutter e como é criado para posteriormente notarmos quais são as diferenças expressivas de um para o outro. Normalmente, tendemos a utilizar rotas como os sites utilizam, já que é um padrão difundido há vários anos na web, mas, diferente dos sites que os usuários têm acesso à URL, em Flutter utilizamos este artifício “por baixo dos panos”.

Independentemente do que utilizamos para navegar, é importante entender que os usuários esperam conseguir ir de uma tela para a outra e voltar. Algumas vezes, precisaremos passar informações de uma tela para a tela seguinte que será aberta (como os detalhes de um produto, por exemplo) ou mesmo passar dados de uma segunda tela para a anterior. Esse fluxo de “vai e vem” dos dados precisa ser tratado com bastante cuidado por nós programadores e programadoras.

A documentação do Flutter traz algumas formas de criar a navegação, sendo elas:

= Navegar até uma nova tela e voltar.

  • Navegar com rotas nomeadas.
  • Navegar passando argumentos para uma rota nomeada.
  • Retornar dados de uma tela.
  • Enviar dados para uma nova tela.

Cada uma dessas formas de navegação tem suas particularidades, diferenças, vantagens e desvantagens. Basicamente, o que precisamos entender agora é que cada uma dessas implementações é uma forma de empilhar, substituir e remover telas abertas ao longo da navegação realizada pelos usuários.

Banner de divulgação da Imersão IA da Alura em colaboração com o Google. Mergulhe em Inteligência artificial com a Alura e o Google. Serão cinco aulas gratuitas para você aprender a usar IA na prática e desenvolver habilidades essenciais para o mercado de trabalho. Inscreva-se gratuitamente agora!

Push e Pop

O mecanismo de pilha, ou seja, o primeiro que é aberto é o último a ser fechado, é amplamente utilizado e difundido no universo Flutter. Além de ser a forma mais simples de navegar entre telas, segundo os próprios criadores do framework, os comandos push e pop são utilizados amplamente para realizar estas ações. Podemos criar um exemplo básico seguindo a documentação do Flutter. A seguir, veja um exemplo implementado com o mecanismo de rotas em push e pop:

import 'package:alura_project/screens/three_screen.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Terceira página"),
      ),
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: ()  {
               Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => ThreeScreen()),
                );
              },
              child: Text('Ir para a página 3 ->'),
            ),

            SizedBox(height: 20),

            ElevatedButton(
              onPressed: ()  {
               Navigator.pop();
              },
              child: Text('<- Voltar para a página 2'),
            ),
          ]
        )
      ),
    );
  }
}

O código acima resultará na seguinte tela:

  

Partindo do exemplo acima, conseguimos criar um botão para avançar uma página e retroceder uma página, de acordo com a necessidade do usuário. Repare que foi necessário o uso do widget MaterialPageRoute para definir como a transição entre as telas acontecerá.

Apesar de bastante simples, esse exemplo tem um problema escondido que logo aparecerá quando o código começar a crescer. E este problema é o acoplamento das duas telas: a tela dois precisa instanciar o widget da tela três. Para isso foi necessário importar o arquivo da tela 3 dentro da tela 2. Repare bem na seguinte linha logo no começo do exemplo:

import 'package:alura_project/screens/three_screen.dart';

Outro detalhe importante, que também está diretamente na tela, é que o tipo de rota a ser utilizada foi o MaterialPageRoute. Imagine um cenário em que as transições de tela mudarem para CoupertinoPageRoute? Ou ainda que decidamos mudar todas as rotas presentes no aplicativo de push e pop para rotas nomeadas? E agora, quem poderá nos defender? :fire:

Talvez você esteja pensando agora se é simples de resolver. É só criar um arquivo com várias funções e cada função retorna o widget da nova tela. Desta maneira conseguiríamos definir separadamente as rotas em um arquivo que pode ser importado pelos widgets que querem navegar entre telas. Por exemplo:

// Código hipotético de um arquivo de navegação
// navegacao.dart

toScreenThree(BuildContext context) => Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => ThreeScreen()),
  );

E na tela (widget) que deseja utilizar a navegação, bastaria importar o arquivo de navegação e chamar os métodos corretos:

import './navegacao.dart';

ElevatedButton(
  onPressed: () => toScreenThree(),
  style: ElevatedButton.styleFrom(
    primary: Colors.purple,
    onPrimary: Colors.white,
  ),
  child: Text('Ir para a página 3 ->'),
),

Com isso, foi possível separar a navegação dos widgets de tela e centralizar em um único ponto. Caso haja necessidade de mudar qual widget vai aparecer quando uma rota for chamada, precisamos apenas modificar na função toScreenThree e todos que a importam vão apontar para o novo widget desejado.

Rotas nomeadas

Uma outra abordagem bastante utilizada é a de nomear as rotas. A documentação do Flutter traz ótimos exemplos de como realizar esta ação. Detalharemos esta abordagem para que fiquem claras as diferenças entre a organização de rotas por pilha e por rotas nomeadas e quais são os diferenciais do Nuvigator.

Podemos criar rotas nomeadas para o nosso exemplo de um aplicativo com três telas da seguinte maneira, dentro do arquivo main.dart:

MaterialApp(
  // O initialRoute é a primeira rota que é aberta no app
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/two': (context) => TwoScreen(),
    '/three': (context) => ThreeScreen(),
  },
);

Dessa forma, nas telas poderíamos chamar a próxima tela com a seguinte abordagem:

RaisedButton(
  child: Text('Abrir tela 3 ->'),
  onPressed: () => Navigator.pushNamed(context, '/three')
),

Com esta abordagem conseguimos declarar rotas em um formato similar ao de um link que encontramos em sites. Dessa maneira, podemos abrir esses links de qualquer lugar do aplicativo através do Navigator.pushNamed(). Porém, pense comigo… Criamos outro acoplamento, agora não diretamente ligado ao código, mas com a string da URL. Cada página carregará a string específica de cada URL. E se precisarmos atualizar? Visitaremos tela por tela, uma a uma, verificando se tudo está nos conformes e com a rota nova? Será que nenhuma seria esquecida nessa atualização?

Quando trabalhamos muito com o "se" na programação, desconfie! Murphy nunca tarda com os programadores e as programadoras. Outro aspecto bastante trabalhoso quando trabalhamos com mudanças de telas e rotas é a passagem de parâmetros. Muitas vezes precisamos passar objetos gigantescos de uma tela para a outra com as informações de um dado produto, por exemplo. Apesar de ser bem explicado na documentação como funciona o mecanismo de passagem de parâmetros entre rotas nomeadas, esta tarefa normalmente acaba sendo árdua pelo aumento do grau de complexidade e do tamanho do código que estamos criando.

É comum precisarmos criar uma classe que formalize os dados que serão passados para a rota, gerar uma nova instância e passar esta instância para a rota. Mas imagine essa burocracia de sempre criar uma classe nova para passar dados para uma nova rota? Conforme a aplicação vai crescendo, imagine como ficará a manutenção do código. Lidar com o efeito “boilerplate” fica cada vez mais complexo.

Nuvigator

A proposta do Nuvigator é abstrair o roteamento que já vem implementado por padrão no Flutter, simplificando a declaração e reutilização das rotas, principalmente à medida que a aplicação cresça. O Nuvigator é uma iniciativa open source criada e disponibilizada pela Nubank através do GitHub. O repositório fornece uma espécie de API declarativa na qual podemos definir rotas, parâmetros, deep links e o que mais precisarmos, enquanto o Nuvigator gera o código para facilitar o uso das rotas.

Atenção: o exemplo a seguir foi construído utilizando o Nuvigator na versão 0.7.2 e o build runner na versão 1.11.0!

O exemplo mais básico, segundo a documentação, é o seguinte:

import 'package:flutter/widgets.dart';
import 'package:nuvigator/nuvigator.dart';
import 'package:flutter/material.dart';

// Este arquivo ainda não existe, então, o seu código mostrará um erro.
// Mantenha este import do tipo part, já já ele será útil!
part 'main.g.dart';

class MyScreen extends StatelessWidget {

  @override
  Widget build(BuildContext) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Alura nuvigator"),
      ),
      body: Center(
        child: Text(
            'Esta tela foi aberta através do nuvigator!',
            style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }
}

@nuRouter
class MainRouter extends NuRouter {

  @NuRoute()
  ScreenRoute myRoute() => ScreenRoute(
    builder: (_) => MyScreen(),
  );

  @override
  Map<RouteDef, ScreenRouteBuilder> get screensMap  => _$screensMap;
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: Nuvigator(
        router: MainRouter(),
        screenType: materialScreenType,
        initialRoute: MainRoutes.myRoute,
      ),
    );
  }
}

Para que o arquivo main.g.dart seja gerado com o código que efetivamente criará o mecanismo de rotas, declare na sessão de dev_dependencies do seu arquivo pubspec.yaml uma extensão chamada build_runner. O seu arquivo deverá ficar similar a este:

name: alura_nuvigator
description: A new Flutter project.

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.0
  nuvigator: ^0.7.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^1.11.0

flutter:
  uses-material-design: true

Agora, execute em seu terminal de comando (na raiz da pasta do projeto Flutter que estamos criando juntos) o seguinte comando:

flutter pub run build_runner build --delete-conflicting-outputs

E, ao fim da execução, no mesmo diretório do arquivo main.dart, deverá aparecer o arquivo main.g.dart. Agora, é só executar o código em um emulador ou dispositivo físico e pronto!

  

Legal! Temos uma página aberta com as rotas do Nuvigator, mas, ficou tudo dentro do arquivo main.dart. Que tal separarmos o projeto em pastas e criar mais telas? Vamos, então, criar a seguinte estrutura de pastas e arquivos dentro da pasta lib:

├───lib
│   ├───navigation
│   ├───────alura_router.dart
│   ├───────alura_router.g.dart
│   ├───screens
│   ├───────home_screen.dart
│   ├───────two_screen.dart
│   ├───────three_screen.dart
│   ├────main.dart

Criados os arquivos e as estruturas de pastas, vamos organizar o projeto por partes! No arquivo main.dart, vamos configurar o Nuvigator no MaterialApp e os devidos imports necessários. Seu arquivo main.dart deverá ficar assim:

import 'package:flutter/widgets.dart';
import 'package:nuvigator/nuvigator.dart';
import 'package:flutter/material.dart';
import 'navigation/alura_router.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: Nuvigator(
        router: AluraRouter(),
        screenType: materialScreenType,
        initialRoute: AluraRoutes.home,
      ),
    );
  }
}

Em seguida, precisamos configurar o alura_router.dart para que nele contenha as especificações que queremos que o Nuvigator gere as rotas necessárias para utilizarmos ao longo do ciclo de funcionamento da aplicação. Seu código deverá ficar similar a este:

import 'package:alura_nuvigator/screens/home_screen.dart';
import 'package:alura_nuvigator/screens/three_screen.dart';
import 'package:alura_nuvigator/screens/two_screen.dart';
import 'package:nuvigator/nuvigator.dart';
import 'package:flutter/widgets.dart';
part 'alura_router.g.dart';

@nuRouter
class  AluraRouter extends NuRouter {

  @NuRoute()
  ScreenRoute<void> home() => ScreenRoute(
    builder: (context) => HomeScreen(),
  );

  @NuRoute()
  ScreenRoute<void> pageTwo() => ScreenRoute(
    builder: (context) => TwoScreen()
  );

  @NuRoute()
  ScreenRoute<void> pageThree({String texto}) => ScreenRoute(
    builder: (context) => ThreeScreen(texto: texto),
  );

  @override
  Map <RouteDef , ScreenRouteBuilder> get screensMap => _$screensMap;
}

O arquivo da página home, que é a primeira página a ser exibida no projeto, deverá ficar próximo ao seguinte código:

import 'package:alura_nuvigator/navigation/alura_router.dart';
import 'package:flutter/material.dart';
import 'package:nuvigator/nuvigator.dart';

class HomeScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    final router = NuRouter.of<AluraRouter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text("Primeira página"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: ()  {

    // É dessa forma que damos um “push” para outra página através do nuvigator! :)
    // As opções de métodos disponíveis podem ser vistas dentro do 
// arquivo gerado pelo Nuvigator
            final router = NuRouter.of<AluraRouter>(context);
            router.toPageTwo();
          },
          child: Text('Ir para a página 2 ->'),
        ),
      ),
    );
  }
}

A página dois será configurada de maneira que aponte para a página três e receba um valor informado pelo usuário e que será passado como parâmetro para a próxima página. O código da página dois ficou assim:

import 'package:alura_nuvigator/navigation/alura_router.dart';
import 'package:flutter/material.dart';
import 'package:nuvigator/nuvigator.dart';

class TwoScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    final router = NuRouter.of<AluraRouter>(context);
    final TextEditingController textoController = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: Text("Segunda página"),
        backgroundColor: Colors.purple,
      ),
      body: Padding(
        padding: const EdgeInsets.all(30.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              decoration: InputDecoration(
                  hintText: 'Diga alguma coisa'
              ),
              controller: textoController,
            ),
            SizedBox(height: 20,),
            ElevatedButton(
              onPressed: () {

    // O método toPageThree e os seus similares para as outras telas 
    // são gerados automaticamente no arquivo alura_router.g.dart
                router.toPageThree(texto: textoController.text);
              },
              style: ElevatedButton.styleFrom(
                primary: Colors.purple, // background
                onPrimary: Colors.white, // foreground
              ),
              child: Text('Ir para a página 3 ->'),
            ),
          ],
        ),
      ),
    );
  }
}

E, por último, mas não menos importante, trabalharemos o código da página três. Nela, recebemos e exibimos ao usuário o valor digitado na tela anterior.

import 'package:alura_nuvigator/navigation/alura_router.dart';
import 'package:flutter/material.dart';
import 'package:nuvigator/nuvigator.dart';

class ThreeScreen extends StatelessWidget {

  final String texto;
  ThreeScreen({Key key, @required this.texto}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    final router = NuRouter.of<AluraRouter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text("Terceira página"),
        backgroundColor: Colors.black,
      ),
      body: Center(
        child: Text(
          this.texto.isEmpty ? 'Nada informado!' : this.texto,
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold
          ),
        ),
      ),
    );
  }
}

Agora, basta gerarmos as rotas do Nuvigator e pronto! Temos uma navegação de três telas passando parâmetros. Não se esqueça de excluir o arquivo main.g.dart que geramos anteriormente para entender como começar a implementação com a biblioteca. Ao executar o build runner, este identificará que precisa gerar o arquivo alura_router.g.dart na pasta navigation.

flutter pub run build_runner build --delete-conflicting-outputs

A aplicação ficará como na imagem abaixo:

  

Conclusão

O Nuvigator traz uma série de vantagens, além das que já foram listadas aqui. É possível trabalhar com rotas aninhadas, deep links, personalizar rotas e afins. Ele foi feito visando automatizar o processo de boilerplate (códigos que se repetem muito e são extremamente similares) e também nos auxilia a não deixar os códigos de rotas espalhados por toda a aplicação. Para aplicativos pequenos pode parecer um tanto burocrático, mas à medida que o seu código crescer, será visível a diferença. O código na íntegra do projeto desenvolvido para este artigo está disponível neste repositório.

Referências

Repositório do Nuvigator.

Leonardo Marinho
Leonardo Marinho

Leonardo é graduado em Análise e Desenvolvimento de Sistemas. Atualmente é mestre em informática pela UFRJ. Desenvolvedor Full Stack apaixonado por criar aplicativos para dispositivos móveis com tecnologias como Ionic e Flutter. Está se aventurando pelo universo da ciência de dados. Organizador da conferência OpenLabs, atualmente a maior conferência tecnológica da região serrana fluminense. É membro fundador da comunidade Dart Lang Brasil. Gosta de Star Wars e Café.

Veja outros artigos sobre Mobile