Flutter Navegación

De MediaWiki
Ir a la navegación Ir a la búsqueda

Intruducción

  • Más información en:


  • El objetivo de esta sección es aprender una forma de 'navegar' entre las diferentes pantallas que haya en una aplicación móbil, bien a través de opciones de menú, bien a través de un Widget al que controlemos un evento que provoque la carga de una nueva pantalla.
La forma en como 'navega' entre pantallas es empleando el concepto de pila. Cada nueva pantalla que se visualiza se pone 'encima' de la anterior mediante una operación de PUSH empleando un objeto Navigator.
Cuando cerramos una pantalla, se realiza la operación contraria y se hace una operación POP sobre la pila, quitando la pantalla actual y visualizando la pantalla anterior.
Veremos que se pueden hacer otro tipo de operaciones, como REPLACE, que reemplzaría la pantalla actual con otra nueva manteniendo la pila como estuviera en ese momento.
  • Por eso es muy importante no abrir nuevas pantallas cuando queramos regresar a una pantalla anterior ya que estaríamos añadiendo nuevas pantallas a la pila, manteniendo las anteriores.



  • Para ello voy a crear un nuevo StatelessWidget MiMaterialAppNavegar el cual va a visualizar una lista de opciones que cargarán diferentes pantallas.
También modificaré el AppBar para que aparezcan opciones de menú y que también se navege entre pantallas al pulsar una de ellas así como el uso de Button Navigation.
Para ello haré uso del snippet fscaffoldabbn el cual genera todo el código necesario.
También voy a crear dos pantallas a las cuales vamos a navegar desde las diferentes opciones.


Flutter dart navegacion 1.JPG


  • La estructura de páginas será la siguiente:
  • /lib/main.dart => Página que ejecuta el main y carga MiMaterialAppConNavegacion
  • /lib/src/pages/app_con_navegación.dart => Clase MiMaterialAppConNavegacion, que visualiza un ManterialApp y carga PrincipalNavegacionPage
  • /lib/src/pages/stateful/navegando/principal_navegación_page.dart => Clase PrincipalNavegacionPage que visualiza una lista para navegar, un Appbar con dos opciones de menú para navegar y dos Navigation Button en la parte inferior para navegar.
  • /lib/src/pages/stateful/navegando/pagina1_page.dart => Clase Pagina1NavegacionPage. Pantalla con un contenido
  • /lib/src/pages/stateful/navegando/pagina2_page.dart => Clase Pagina2NavegacionPage. Pantalla con un contenido.


Archivo main.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/app.dart';
import 'src/app_con_navegacion.dart';

void main() => runApp(MiMaterialAppConNavegacion());


Archivo app_con_navegacion.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/pages/stateful/navegando/principal_navegacion_page.dart';

class MiMaterialAppConNavegacion extends StatelessWidget {
  const MiMaterialAppConNavegacion({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PrincipalNavegacionPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}


Archivo PrincipalNavegacionPage.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina1_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina2_page.dart';

class PrincipalNavegacionPage extends StatefulWidget {
  const PrincipalNavegacionPage({Key? key}) : super(key: key);

  @override
  _PrincipalNavegacionPageState createState() => _PrincipalNavegacionPageState();
}

class _PrincipalNavegacionPageState extends State<PrincipalNavegacionPage> {
  int _index = 0; // TODO: make sure this is outside build(), otherwise every setState will change the value back to 0

  @override
  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(
        title: Text('App con navegación'),
        actions: [
          IconButton(icon: Icon(Icons.av_timer),onPressed: (){},),
          IconButton(icon: Icon(Icons.add),onPressed: (){},),

        ],
      ),

      body: _pantallasCargar(),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (tappedItemIndex) =>
            setState(() {
              _index = tappedItemIndex;
            }),
        currentIndex: _index,
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.av_timer), label: 'navBarItem1Text'),
          BottomNavigationBarItem(
              icon: Icon(Icons.add), label: 'navBarItem2Text')
        ],
      ),
    );
  }

  Widget _pantallasCargar(){

    return ListView(
      children: [
        ListTile(title: Text('Pantalla 1'), trailing: Icon(Icons.av_timer),
                onTap: (){},),
        ListTile(title: Text('Pantalla 2'), trailing: Icon(Icons.add),
          onTap: (){},),
      ],

    );

  }
}


Archivo pagina1_page.dart:

import 'package:flutter/material.dart';

class Pagina1NavegacionPage extends StatelessWidget {
  const Pagina1NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Página 1'),
        ),
        body: null
    );
  }
}


Archivo pagina2_page.dart:

import 'package:flutter/material.dart';

class Pagina2NavegacionPage extends StatelessWidget {
  const Pagina2NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Página 2'),
        ),
        body: null
    );
  }
}




Navegando entre pantallas

  • Más información en:





Archivo principal_navegacion_page.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina1_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina2_page.dart';


class PrincipalNavegacionPage extends StatefulWidget {
  const PrincipalNavegacionPage({Key? key}) : super(key: key);

  @override
  _PrincipalNavegacionPageState createState() => _PrincipalNavegacionPageState();
}

class _PrincipalNavegacionPageState extends State<PrincipalNavegacionPage> {
  int _index = 0; // TODO: make sure this is outside build(), otherwise every setState will change the value back to 0

  @override
  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(
        title: Text('App con navegación'),
        actions: [
          IconButton(icon: Icon(Icons.av_timer),onPressed: (){},),
          IconButton(icon: Icon(Icons.add),onPressed: (){},),

        ],
      ),

      body: _pantallasCargar(),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (tappedItemIndex) =>
            setState(() {
              _index = tappedItemIndex;
            }),
        currentIndex: _index,
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.av_timer), label: 'navBarItem1Text'),
          BottomNavigationBarItem(
              icon: Icon(Icons.add), label: 'navBarItem2Text')
        ],
      ),
    );
  }

  Widget _pantallasCargar(){

    return ListView(
      children: [
        ListTile(title: Text('Pantalla 1'), trailing: Icon(Icons.av_timer),
                onTap: (){
                  final ruta = MaterialPageRoute(
                      builder: (context) => Pagina1NavegacionPage(),);

                  Navigator.of(context).push(ruta);
                },),
        ListTile(title: Text('Pantalla 2'), trailing: Icon(Icons.add),
          onTap: (){},),
      ],

    );

  }
}


Archivo pagina1_page.dart:

import 'package:flutter/material.dart';

class Pagina1NavegacionPage extends StatelessWidget {
  const Pagina1NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).pop();
        },
        child: Icon(Icons.arrow_back),
      ),
        appBar: AppBar(
          title: Text('Página 1'),
        ),
        body: null
    );
  }
}



Navegación usando rutas


Navegación imperativa

En este tipo de navegación, los Widgets que conforman las pantallas, se comportan como hojas que se apiladas unas encima de otras.
Debemos entender que la hoja (pantalla = widget) que está encima de todo es la pantalla que se está visualizando en este momento.
Podemos hacer operaciones de 'push' para apilar una nueva hoja=pantalla, operaciones de 'pop' para quitar una hoja de encima de la pila u operaciones de replace, para cambiar una hoja por otra.






Archivo routes.dart:

import 'package:flutter/material.dart';

import 'package:holamundo/src/pages/stateful/navegando/pagina1_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina2_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/principal_navegacion_page.dart';

Map<String,WidgetBuilder>obtenerRutas(){

  return <String,WidgetBuilder>{
    'home' : (BuildContext context) => PrincipalNavegacionPage(),
    'pagina1' : (BuildContext context) => Pagina1NavegacionPage(),
    'pagina2' : (BuildContext context) => Pagina2NavegacionPage(),
  };

}


Archivo app_con_navegacion.dart:

import 'package:flutter/material.dart';

import 'package:holamundo/src/pages/stateful/navegando/default_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/principal_navegacion_page.dart';
import 'package:holamundo/src/routes/routes.dart';

class MiMaterialAppConNavegacion extends StatelessWidget {
  const MiMaterialAppConNavegacion({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: null,
      debugShowCheckedModeBanner: false,
      initialRoute: 'home',
      routes: obtenerRutas(),
      onGenerateRoute: (RouteSettings routesettings){
        print(routesettings.name);

        return MaterialPageRoute(
            builder: (BuildContext context) => DefaultNavegacionPage());
      },
    );
  }
}


Archivo principal_navegacion_page.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina1_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina2_page.dart';


class PrincipalNavegacionPage extends StatefulWidget {
  const PrincipalNavegacionPage({Key? key}) : super(key: key);

  @override
  _PrincipalNavegacionPageState createState() => _PrincipalNavegacionPageState();
}

class _PrincipalNavegacionPageState extends State<PrincipalNavegacionPage> {
  int _index = 0; // TODO: make sure this is outside build(), otherwise every setState will change the value back to 0

  @override
  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(
        title: Text('App con navegación'),
        actions: [
          IconButton(icon: Icon(Icons.av_timer),onPressed: (){},),
          IconButton(icon: Icon(Icons.add),onPressed: (){},),

        ],
      ),

      body: _pantallasCargar(),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (tappedItemIndex) =>
            setState(() {
              _index = tappedItemIndex;
            }),
        currentIndex: _index,
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.av_timer), label: 'navBarItem1Text'),
          BottomNavigationBarItem(
              icon: Icon(Icons.add), label: 'navBarItem2Text')
        ],
      ),
    );
  }

  Widget _pantallasCargar(){

    return ListView(
      children: [
        ListTile(title: Text('Pantalla 1'), trailing: Icon(Icons.av_timer),
                onTap: () =>  Navigator.of(context).pushNamed('pagina1')
                ,),
        ListTile(title: Text('Pantalla 2'), trailing: Icon(Icons.add),
                onTap: () =>  Navigator.of(context).pushNamed('pagina2')
          ,),
        ListTile(title: Text('Pantalla por defecto'), trailing: Icon(Icons.backpack),
          onTap: () =>  Navigator.of(context).pushNamed('noexiste')
          ,),
      ],

    );

  }
}


Archivo default_page.dart:

import 'package:flutter/material.dart';

class DefaultNavegacionPage extends StatelessWidget {
  const DefaultNavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Pantalla por defecto'),
        ),
        body: null
    );
  }
}


  • Nota: Indicar que Navigator tiene más métodos que push y pop para manejar el cambio de pantallas. Por ejemplo, podemos hacer uso del método pushReplacementNamed (o su equivalente sin emplear los nombres de rutas) para sustituír la pantalla actual por otra.
Aplicado a nuestro ejemplo:

Archivo pagina1_page.dart:

import 'package:flutter/material.dart';

class Pagina1NavegacionPage extends StatelessWidget {
  const Pagina1NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).pop();
        },
        child: Icon(Icons.arrow_back),
      ),
        appBar: AppBar(
          title: Text('Pantalla 1'),
        ),
        body: Center(
          child: TextButton(
            child: Text('PULSAME PARA REEMPLAZAR ESTA PANTALLA POR LA PANTALLA 2',
                        style: TextStyle(backgroundColor: Colors.green,fontSize: 20,color: Colors.white),),
            onPressed: (){
              Navigator.of(context).pushReplacementNamed('pagina2');
            },
          ),
        )
    );
  }
}


Al pulsar sobre el botón, la página1 es sustituída por la página2 y si volvemos atrás pulsando el botón Back, aparecerá la pantalla inicial.


Navegación declarativa


  • A partir de Flutter 2.0 se ha añadido esta nueva forma de navegación entre Widgets.
Este modo de navegación es mejor soportado por la plataforma de tipo Web.



Paso de datos entre pantallas

  • Con lo aprendido en la sección anterior es muy sencillo pasar datos de una pantalla a otra.
Después del nombre de la ruta, sólo hay que indicar el objeto (dato) que queremos pasar. Puede ser cualquier cosa, desde un String hasta una lista de objetos.
 Navigator.of(context).pushNamed('pagina1',arguments: 'DATO DESDE PRINCIPAL A PANTALLA 1')
Va en la propiedad arguments de pushNamed.


  • Una vez abre el Widget que conforma la primera pantalla, para recoger el valor tenemos que escribir lo siguiente en el método built del mismo:
 final args = ModalRoute.of(context)!.settings.arguments as String;  // Convertimos con la cláusula as. Como enviamos una cadena es un String. Podría ser caulquier tipo de dato
Fijarse que es necesario realizar una conversión al tipo de dato enviado. Así, en el ejemplo es un String. Tiene que ser el mismo tipo de dato que el empleado en la operación de pushNamed.



  • También es posible 'recibir' información desde otra pantalla (Widget) al cerrarse.
Para ello es necesario enviar el dato en el método pop de la clase Navigator:
 Navigator.of(context).pop('PULSADO ADD'),


  • Ahora, desde la pantalla que llamó a la anterior que se ha cerrado, debemos de recuperar la información enviada de vuelta.
Para ello sólo tenemos que igual la variable a la llamada al método pushNamed.
Como la operación es asíncrona, deberemos de realizar el proceso dentro de un método modificado con 'async' y que espere (await) el resultado de la llamada (recordar que ya vimos en esta wiki como emplear el await/async).
  void _abrirPaginaPorDefectoConDatosDeVuelta() async {
    final datosDevuelta = await Navigator.of(context).pushNamed('noexiste');  // Es una tarea asíncrona.
    print(datosDevuelta);
  }



  • Aplicado este a nuestro ejemplo, vamos a pasar un dato (String) desde la pantalla principal a la página1 y a la página2. En dichas pantallas vamos a visualizar el texto enviado.
Además, desde la página1, como va a poder reemplazarse por la página2, también vamos a enviar una cadena en ese caso.
Por otra parte, vamos a hacer que la página por defecto envíe de vuelta a la principal un dato (String) en función de un IconButton pulsado.

Archivo pagina1_page.dart:

import 'package:flutter/material.dart';

class Pagina1NavegacionPage extends StatelessWidget {
  const Pagina1NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as String;  // Convertimos con la cláusula as. Como enviamos una cadena es un String. Podría ser caulquier tipo de dato

    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).pop();
        },
        child: Icon(Icons.arrow_back),
      ),
        appBar: AppBar(
          title: Text('Página 1'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(args),
              TextButton(
                child: Text('PULSAME PARA REEMPLAZAR ESTA PANTALLA POR LA PAGINA 2',
                            style: TextStyle(backgroundColor: Colors.green,fontSize: 20,color: Colors.white),),
                onPressed: (){
                  Navigator.of(context).pushReplacementNamed('pagina2',arguments: 'Datos enviados desde página1 a página2');
                },
              ),
            ],
          ),
        )
    );
  }
}


Archivo pagina2_page.dart:

import 'package:flutter/material.dart';

class Pagina2NavegacionPage extends StatelessWidget {
  const Pagina2NavegacionPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as String;  // Convertimos con la cláusula as. Como enviamos una cadena es un String. Podría ser caulquier tipo de dato

    return Scaffold(
        appBar: AppBar(
          title: Text('Página 2'),
        ),
        body: Center(
          child: Text(args),
        )
    );
  }
}


Archivo principal_navegacion_page.dart:

import 'package:flutter/material.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina1_page.dart';
import 'package:holamundo/src/pages/stateful/navegando/pagina2_page.dart';


class PrincipalNavegacionPage extends StatefulWidget {
  const PrincipalNavegacionPage({Key? key}) : super(key: key);

  @override
  _PrincipalNavegacionPageState createState() => _PrincipalNavegacionPageState();
}

class _PrincipalNavegacionPageState extends State<PrincipalNavegacionPage> {
  int _index = 0; // TODO: make sure this is outside build(), otherwise every setState will change the value back to 0

  @override
  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(
        title: Text('App con navegación'),
        actions: [
          IconButton(icon: Icon(Icons.av_timer),onPressed: (){

          },),
          IconButton(icon: Icon(Icons.add),onPressed: (){},),

        ],
      ),

      body: _pantallasCargar(),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (tappedItemIndex) =>
            setState(() {
              _index = tappedItemIndex;
            }),
        currentIndex: _index,
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.av_timer), label: 'navBarItem1Text'),
          BottomNavigationBarItem(
              icon: Icon(Icons.add), label: 'navBarItem2Text')
        ],
      ),
    );
  }

  Widget _pantallasCargar(){

    return ListView(
      children: [
        ListTile(title: Text('Pantalla 1'), trailing: Icon(Icons.av_timer),
                onTap: () =>  Navigator.of(context).pushNamed('pagina1',arguments: 'DATO DESDE PRINCIPAL A PAGINA 1')
                ,),
        ListTile(title: Text('Pantalla 2'), trailing: Icon(Icons.add),
                onTap: () =>  Navigator.of(context).pushNamed('pagina2',arguments: 'DATO DESDE PRINCIPAL A PAGINA 2')
          ,),
        ListTile(title: Text('Pantalla por defecto'), trailing: Icon(Icons.backpack),
          onTap: () {
              _abrirPaginaPorDefectoConDatosDeVuelta();
            }
          ,),
      ],

    );

  }

  void _abrirPaginaPorDefectoConDatosDeVuelta() async {
    final datosDevuelta = await Navigator.of(context).pushNamed('noexiste');  // Es una tarea asíncrona.
    print(datosDevuelta);
  }

}






Enlace a la página principal de la UD5

Enlace a la página principal del curso




-- Ángel D. Fernández González -- (2021).