latest

Programación basada en widgets. En profundidad: Navegación empleando el widget Drawer.

En la lección de hoy de nuestro curso de Flutter vamos a construir un cajón de navegación (Drawer), muy básico a nivel estético, pero que nos permite seguir asentando conocimientos sobre la navegación entre rutas en Flutter. En este caso, emplearemos este patrón de navegación muy común en aplicaciones que siguen la guía de estilo de Material Design.  

Como todos ya sabréis, el cajón de navegación es un panel de navegación cuyas transiciones tienen lugar desde el borde izquierdo de la pantalla, donde se muestran las principales opciones / secciones de navegación de la aplicación. El que nosotros construiremos tiene el siguiente aspecto:

El esqueleto del cajón de navegación se implementa sólo en la ruta inicial (home) de la aplicación, como ya sabéis, la primera pantalla que se muestra al lanzar la app.

Nosotros hemos decidido construir el widget para la ruta home en un nuevo archivo Dart separado (ruta_home.dart) dentro de un paquete que, en esta ocasión, hemos denominado secciones y donde también estarán los archivos del resto de rutas accesibles desde el drawer, así como widgets reutilizados de sesiones anteriores.

Así pues, en un primer momento, el contenido de lib/main.dart, encargado de iniciar la aplicación será el siguiente:

import 'package:flutter/material.dart'; 
// Sustituir "navigation_drawer_ejemplo" por el nombre de vuestro proyecto
import 'package:navigation_drawer_ejemplo/secciones/ruta_home.dart';

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

En el código anterior, se especifica que el widget RutaHome será el que ocupe la ruta inicial. El contenido de lib/secciones/ruta_home.dart, donde se construye dicho widget y que, por ende, define el esqueleto del Drawer es el mostrado a continuación:

import 'package:flutter/material.dart'; 
// Widget reutilizado de sesiones anteriores (añadido al paquete "secciones")
import 'package:navigation_drawer_ejemplo/secciones/votodirecto.dart';

class RutaHome extends StatefulWidget {
  @override
  _RutaHomeState createState() => _RutaHomeState();
}

class _RutaHomeState extends State<RutaHome> {

  ListView _listaMenu;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _listaMenu = new ListView(
      children: <Widget>[
        new DrawerHeader(
          child: new Text("Ajustes"),
        ),
        new ListTile(
          leading: new Icon(Icons.settings),
          title: new Text("Configuración"),
        ),
        new ListTile(
          leading: new Icon(Icons.home),
          title: new Text("Home"), 
        ),
        new ListTile(
          leading: new Icon(Icons.person),
          title: new Text("Opinión personal"),
        ),
        new AboutListTile(
          child: new Text("Info"),
          applicationIcon: new Icon(Icons.info),
          icon: new Icon(Icons.info),
          applicationName: "ExprexaOpinion",
          applicationVersion: "v1.0",
        )
      ]
    ); 
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Voto directo'),
        backgroundColor: Colors.orangeAccent,
      ),
      drawer: new Drawer(
        child: _listaMenu
      ), 
      body: new VotoDirecto(),
    );
  }
}

El código anterior define el Drawer pero todavía no desencadena ninguna acción (cambio de la ruta visible). Es decir, tan solo despliega el cajón de navegación al pulsar el correspondiente icono. El código es extremadamente simple. Sobre el Scaffold que incorpora el método build() para el estado del StatefulWidget RutaHome se hace uso de la propiedad drawery se construye un widget de clase Drawer. Este widget admite en su propiedad child otro widget, en este caso, un ListView que hemos denominado _listaMenu y que ha sido inicializado previamente en el método sobreescrito initState().

El widget ListView tiene una propiedad children que contiene una lista con los widgets de nuestro menú lateral. En este caso, un DrawerHeader , que se ubica en la parte superior del cajón de navegación con el texto "Ajustes"; una serie de widgets ListTile compuestos por iconos y texto que permiten la navegación hacia el resto de rutas; y por último, un widget AboutListTile, que es usado comúnmente para mostrar información relativa al desarrollo de la aplicación (incluye, por ejemplo, un texto para ser mostrado en la entrada del Drawer, una icono también para la entrada del Drawer e información adicional que se muestra en el cuadro de dialogo modal al presionar: el nombre de la aplicación, su versión, el icono de la aplicación, etc). La siguiente captura muestra un ejemplo del cuadro de diálogo desplegado para esta aplicación.

No obstante, el código anterior puede mejorarse notablemente dado que contiene patrones que se repiten varias veces. Además, requiere ser adaptado para facilitar la navegación hacia otras rutas al presionar cada uno de los ListTile. El contenido de lib/secciones/ruta_home.dart, en esta segunda iteración de desarrollo, quedaría como se expone a continuación:

import 'package:flutter/material.dart'; 
// Widget reutilizado de sesiones anteriores (añadido al paquete "secciones")
import 'package:navigation_drawer_ejemplo/secciones/votodirecto.dart';

class RutaHome extends StatefulWidget {
  @override
  _RutaHomeState createState() => _RutaHomeState();
}

class _RutaHomeState extends State<RutaHome> {

  ListView _listaMenu;

  ListTile _construirItem(BuildContext context, IconData iconData , 
   String texto, String ruta){
    return new ListTile(
      leading: new Icon(iconData),
      title: new Text(texto),
      onTap: () {
        setState(() {
           Navigator.pushNamed(context, ruta);  
        });
      }
    );
  }

  ListView _construirListView(BuildContext context){
    return new ListView(
      children: <Widget>[
        new DrawerHeader(
          child: new Text("Ajustes"),
        ),
        _construirItem(context, Icons.settings, "Configuración", "/configuracion"),
        _construirItem(context, Icons.home, "Home", "/"),
        _construirItem(context, Icons.person, "Opinión personal", "/opinionpersonal"),
        new AboutListTile(
          child: new Text("Info"),
          applicationIcon: new Icon(Icons.info),
          icon: new Icon(Icons.info),
          applicationName: "ExpresaOpinion",
          applicationVersion: "v1.0",
        )
      ]
    );   
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Voto directo'),
        backgroundColor: Colors.orangeAccent,
      ),
      drawer: new Drawer(
        child: _construirListView(context)
      ), 
      body: new VotoDirecto(),
    );
  }
}

Como podéis comprobar en el código, los cambios que se han realizado están relacionados con la construcción del widget ListView; y en su mayor parte,  con la forma en la que se componen ahora los widgets ListTile. Se ha implementado un método de acceso protegido _construirListView(BuildContext contex) que retorna el ListView resultante para el cajón de navegación. Cada uno de los items de la lista (ListTile) se construye invocando el método protegido _construirItem(BuildContext context, IconData iconData, String texto, String ruta), que requiere como parámetros: el contexto del widget, los datos del icono, así como el texto a mostrar en la opción y por último, una cadena que representa la ruta hacia la que se navegará.

Prestad atención a la propiedad onTap añadida en los widgets ListTile. Al pulsar sobre el elemento ListTile apilamos en la pila de navegación la ruta correspondiente. En esta ocasión, no hacemos uso de la llamada Navigator.push(BuildContext context, Route<T> route), como sí hicimos en la sesión anterior; sino que optamos por utilizar la variante Navigator.pushNamed(BuildContext context,String routeName,{Object arguments}). Lo interesante en este caso es que la ruta se especifica como una cadena constituida por niveles y barras de tipo slash, estando la ruta inicial (home) representada por una única barra (/). Además, tenemos definidas dos rutas adicionales para la pantalla de configuración (/configuracion) y para el área de opinión personal (/opinionpersonal). Observad también como hemos encapsulado la llamada a Navigator con el método setState(), asociado a los widgets con estado, para notificar al framework sobre la necesidad de renderizar una nueva ruta.

Ahora bien, si no asociamos cada ruta con su jerarquía de widgets correspondientes, nuestro Drawer no realizará adecuadamente las transiciones entre pantallas y sus renderizados. Para ello, tenemos que volver a modificar el archivo main.dart de la siguiente forma:

import 'package:flutter/material.dart'; 
// Sustituir "navigation_drawer_ejemplo" por el nombre de vuestro proyecto
import 'package:navigation_drawer_ejemplo/secciones/ruta_home.dart';
import 'package:navigation_drawer_ejemplo/secciones/ruta_configuracion.dart';
import 'package:navigation_drawer_ejemplo/secciones/ruta_opinion_personal.dart';

void main(){
  runApp(new MaterialApp(
    home: new RutaHome(),
    routes: <String, WidgetBuilder>{
      RutaConfiguracion.nombreRuta: 
        (BuildContext context) => new RutaConfiguracion(),
      RutaOpinionPersonal.nombreRuta: 
        (BuildContext context) => new RutaOpinionPersonal(),  
    }
  ));
}

En primer lugar, se realizan las importaciones necesarias de clases del paquete secciones para tener acceso a los widgets asociados con cada Route (el código de cada uno de estos archivos será proporcionado más adelante).

La novedad más relevante, respecto de la versión previa, es la utilización de la propiedad routes que requiere de un diccionario (Map) con los nombres de las rutas como "claves" ("/","/configuracion","/opinionpersonal") y elementos WidgetBuilder como "valor". En la definición de cada WidgetBuilder se especifica el widget a renderizar asociado a esa ruta en particular. Se han usado variables estáticas de tipo cadena para guardar las cadenas de las rutas y mantenerlas encapsuladas en las clases que las representan. Para terminar, mostramos el código de  lib/secciones/ruta_configuracion.dart y lib/secciones/ruta_opinion_personal.dart de modo que tengáis a vuestra disposición todo el código necesario para reproducir este ejemplo.

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

import "package:flutter/material.dart";

class RutaConfiguracion extends StatelessWidget {
 
  // Variable estática que se usa en main.dart (propiedad routes)
  static const nombreRuta = "/configuracion";

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Configuración de la App"),
        backgroundColor: Colors.orangeAccent,
      ),
      body: new Container(
        child: new Center(
          child: new Text("Widget de configuración..."),
        )
      )
    );
  }
}

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

import "package:flutter/material.dart";
// Widget reutilizado de sesiones anteriores (añadido al paquete "secciones")
import "package:navigation_drawer_ejemplo/secciones/opinionpersonal.dart";

class RutaOpinionPersonal extends StatelessWidget {

  // Variable estática que se usa en main.dart (propiedad routes)
  static const nombreRuta = "/opinionpersonal";

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Danos tu opinión"),
        backgroundColor: Colors.orangeAccent,
      ),
      body: new AreaOpinionPersonal() //widget reutilizado de sesiones anteriores
    );
  }
}

En este punto, deberíamos tener un ejemplo completamente funcional de cajón de navegación. La imagen siguiente muestra todas las pantallas de esta aplicación de ejemplo.

Continuaremos el curso estudiando los widgets ListView con mayor profundidad.

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