latest

Programación basada en widgets. En profundidad: Navegación entre rutas.

En esta nueva lección, por primera vez, vamos a realizar navegación entre dos rutas (o entre dos pantallas) de la aplicación usando Navigator. En Android, una ruta (route) encontraría su equivalente en una Activity. En iOS, una ruta sería un elemento ViewController. Si bien, en Flutter una ruta es un widget, ¿Cómo podemos navegar a una nueva ruta? La respuesta es usando Navigator.

Así pues, en este ejemplo del curso de Flutter realizaremos la navegación entre dos rutas, pudiendo movernos de una a otra usando los métodos Navigator.push() y Navigator.pop(). Como venimos haciendo en las sesiones anteriores vamos a reutilizar nuestro ejemplo de votaciones sobre un curso para convertirlo en un ejemplo sencillo de navegación entre dos rutas.

La interfaz de usuario quedará tal y como se muestra  a continuación:

La navegación entre rutas en Flutter se implementa como un stack o pila de widgets Route. Para lanzar una nueva pantalla es necesario apilar una nueva Route usando Navigator.push(BuildContext context, Route<T> route). El context, lo obtenemos directamente del método build() de _HomeRutaState; la ruta (route), por su parte, la construiremos previamente incluyendo el widget a renderizar que constituirá la nueva pantalla.

En nuestro caso, al usar Material Design, implementaremos rutas de la clase MaterialPageRoute({@required WidgetBuilder builder, RouteSettings settings, bool maintainState: true, bool fullscreenDialog: false }). No vamos a entrar en los detalles específicos de los parámetros opcionales del constructor anterior dado que, por ahora, en nuestros ejemplos sólo vamos a utilizar el parámetro requerido WidgetBuilder builder. Si queréis saber más sobre MaterialPageRoute os recomendamos visitar la documentación de Flutter.

Por otra parte, para regresar a la pantalla anterior debe ejecutarse Navigator.pop(BuildContext context). Esta extracción de la pila de rutas, como veremos, la podemos realizar explícitamente, por ejemplo, presionando un RaisedButton; o si nuestro widget renderizado, fruto de un push() anterior, contiene un Scaffold con un widget AppBar, éste incorporará por defecto un IconButton con una flecha de retorno para regresar a la ruta anterior (home). La flecha de retorno ejecutará un pop() implícitamente.

El código de lib/main.dart es el siguiente:

import 'package:flutter/material.dart';
// sustituir "navegacion_rutas" por el nombre de vuestro proyecto
import 'package:navegacion_rutas/rutas/votodirecto.dart';
// sustituir "navegacion_rutas" por el nombre de vuestro proyecto
import 'package:navegacion_rutas/rutas/opinionpersonal.dart';

void main() {
  runApp(
    new MaterialApp(
      home: new HomeRuta(),
    ),
  );
}

class HomeRuta extends StatefulWidget {
  @override
  _HomeRutaState createState() => _HomeRutaState();
}

class _HomeRutaState extends State<HomeRuta> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
        title: new Text("¿Qué os parece el curso?"),
        backgroundColor: Colors.orangeAccent,
        actions: <Widget>[
          new IconButton(
            icon: Icon(Icons.person),
            onPressed: (){
              print("Has presionado el botón de opinión personal");
              final opinionPersonalRoute = new MaterialPageRoute(
                builder: (context)=> new Scaffold(
                  appBar: new AppBar(
                    title: new Text("Haznos llegar tu opinión personal"),
                    backgroundColor: Colors.orangeAccent,
                  ),
                  body:AreaOpinionPersonal()
                )
              );
              Navigator.push(context, opinionPersonalRoute);
            },
        )],
      ),
      body: new VotoDirecto(),
    );
  }
}

En el método build(), dentro del estado del stateful widget HomeRuta, encontramos los widgets y sentencias necesarias para navegar desde la ruta home hacia una nueva ruta. Básicamente, en este ejemplo hemos implementado un botón de tipo icono (IconButton) en el AppBar aprovechando el espacio destinado para este tipo de botones que proporciona la propiedad  actions: <Widget>[]. El VoidCallback definido en onPressed, primero, construye la ruta (MaterialPageRoute opinionPersonalRoute) y luego la utiliza junto con la información de contexto del widget para apilarla en la pila de rutas de navegación con la sentencia Navigator.push(context,opinionPersonalRoute).

Prestad atención a la implementación del callback en la propiedad builder del MateriaPageRoute que hemos construido. Requiere el contexto del widget (context) como argumento. En el cuerpo del callback, se construye el widget que se renderizará en la nueva ruta.

El código de lib/votodirecto.dart es el siguiente (sin cambios relevantes):

import 'package:flutter/material.dart';

class VotoDirecto extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return new Container(
      child:new Center(
        child: new Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            new MiCardStateful(
              titulo: "Me gusta",
              color: Colors.greenAccent,
              datosIcono: Icons.thumb_up,
            ),
            new MiCardStateful(
              titulo: "No me gusta",
              color: Colors.redAccent,
              datosIcono: Icons.thumb_down),
          ],
        ),
      ),
    );
  }
}

class MiCardStateful extends StatefulWidget {
  // Propiedades estáticas
  final String titulo;
  final IconData datosIcono;
  final num tamanyoIcono;
  final Color color;

  // Constructor
  MiCardStateful(
      {this.titulo, this.datosIcono, this.tamanyoIcono = 40.0, this.color});

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

class _MiCardStatefulState extends State<MiCardStateful> {
  //Propiedades dinámicas:
  num _contador = 0;

  void _incrementContador() {
    setState(() {
      _contador++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.only(top: 6.0),
      child: new Card(
          child: Container(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: <Widget>[
                  Icon(widget.datosIcono,
                      color: widget.color, size: widget.tamanyoIcono),
                  Padding(
                    // padding entre boton e icono
                    padding: EdgeInsets.all(6.0),
                  ),
                  RaisedButton(
                    child: Text(
                      widget.titulo,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                    onPressed: _incrementContador,
                    color: widget.color,
                    padding: EdgeInsets.all(16.0), // padding interno del botón
                  ),
                  Padding(
                    padding: EdgeInsets.all(6.0),
                  ),
                  Text(_contador.toString(),
                      style: TextStyle(
                        fontSize: 18.0,
                      )), // Otra manera: Text('$_contador')
                ],
             )
          )
       ),
    );
  }
}

El código de lib/opinionpersonal.dart es el siguiente:

import "package:flutter/material.dart";

enum Acciones { si, no }

class AreaOpinionPersonal extends StatefulWidget {
  @override
  _AreaOpinionPersonalState createState() => _AreaOpinionPersonalState();
}

class _AreaOpinionPersonalState extends State<AreaOpinionPersonal>  {
  String _opinion = ""; // Propiedad dinámica
  String _ultOpinionEnviada = ""; // Propiedad dinámica

  TextEditingController opinionController = new TextEditingController();

  void _realizarAccion(Acciones accion) {
    if (accion == Acciones.si) {
      print("Tu opinión personal ha sido enviada");
      setState(() {
        opinionController.clear();
        // Sería Lo mismo que:
        // opinionController.text="";
        _ultOpinionEnviada = _opinion;
        _opinion = "";
      });
    } else {
      print("No se ha mandado tu opinión");
    }
  }

  void _mostrarDialogo() {
    AlertDialog dialogo = new AlertDialog(
      content: new Text('¿Seguro que deseas expresar tu opinión?\n\n'
          '\"$_opinion\"'),
      actions: <Widget>[
        new FlatButton(
            onPressed: () {
              _realizarAccion(Acciones.si);
              // Para cerrar el cuadro de dialogo
              Navigator.pop(context);
            },
            child: new Text("Sí")),
        new FlatButton(
            onPressed: () {
              // Para cerrar el cuadro de dialogo
              _realizarAccion(Acciones.no);
              Navigator.pop(context);
            },
            child: new Text("No"))
      ],
    );
    showDialog(context: context, child: dialogo);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        padding: const EdgeInsets.all(8.0),
        child: new Center(
            child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(
                hintText: "Introduce tu opinión personal o lo que quieras...",
              ),
              onChanged: (String data) {
                _opinion = data;
              },
              maxLines: 2,
              controller: opinionController,
            ),
            Padding(
              padding: const EdgeInsets.all(5.0),
            ),
            RaisedButton(
                child: new Text("Enviar opinión personal"),
                onPressed: () {
                  _mostrarDialogo();
                }),
            Text("Última opinión enviada: $_ultOpinionEnviada"),
            Padding(
              padding: const EdgeInsets.all(15.0),
            ),
             RaisedButton(
                child: new Text("Volver al voto directo"),
                onPressed: () {
                  Navigator.pop(context);
                }
             ),
          ],
        )
      )
    );
  }
}

Como podéis comprobar, en el método build() del _AreaOpinionPersonalState se ha añadido un widget RaisedButton con un VoidCallback para el evento / propiedad de onPressed que únicamente realiza un pop() de manera explícita para regresar a la ruta inicial (home) si se presiona dicho botón. Además, si ejecutáis esta aplicación en el emulador o en el terminal, podéis comprobar que cuando se muestra en pantalla la ruta con el widget AreaOpinionPersonal, la implementación por defecto incluye un IconButton con una flecha de retorno a la ruta previa (la que hizo el push()).

Hasta aquí esta primera aproximaxión a la navegación entre rutas con Navigator. Más adelante, realizaremos modificaciones al código anterior para mejorar su comportamiento y que el widget de AreaOpinionPersonal no pierda sus datos generados con anterioridad (previo pop() y nuevo push()desde la ruta inicial).

Author image
Iván González is postdoctoral researcher at the Castilla-La Mancha University.
Ciudad Real (Spain)