latest

Introducción a la programación basada en widgets. Navegación usando Pestañas

Continuamos con nuestro curso de Flutter. En esta lección nos centraremos en el patrón de navegación basado en pestañas, muy utilizado en las GUIs de aplicaciones móviles. Para ello, Flutter proporciona los widgets de Material Design TabBar, que podemos incluir en la propiedad bottom de un widget AppBar y TabBarView, que integraremos en la propiedad body de un Scaffold.

Para mostrar un ejemplo de utilización de la navegación con pestañas vamos a realizar modificaciones la GUI de la lección anterior, separando en dos tabs las opciones de opinión. Una primera pestaña para los votos directos ("Me gusta" / "No me gusta") y una segunda pestaña para enviar las opiniones personales. La interfaz de usuario quedará tal y como se muestra  a continuación:

A continuación os adjuntamos todo el código de este ejemplo. En este caso la interfaz de usuario tiene (por primera vez) varios archivos fuente: lib/main.dart, lib/pestanyas/votodirecto.dart y lib/pestanyas/opinionpersonal.dart. Como podéis observar se ha creado un subdirectorio (pestanyas) en el directorio lib que contiene dos archivos fuentes, cada uno de ellos define los widgets que contendrá cada pestaña.

El contenido de lib/main.dart:

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

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

class GestorTabs extends StatefulWidget {
  @override
  _GestorTabsState createState() => _GestorTabsState();
}

class _GestorTabsState extends State<GestorTabs>
    with SingleTickerProviderStateMixin {
  TabController controlador;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controlador = new TabController(
      length: 2,
      vsync: this,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
        title: new Text("¿Qué os parece el curso?"),
        backgroundColor: Colors.orangeAccent,
        bottom: TabBar(
          tabs: <Widget>[
            new Tab(
              icon: Icon(Icons.thumbs_up_down),
            ),
            new Tab(
              icon: Icon(Icons.person),
            ),
          ],
          controller: controlador,
        ),
      ),
      body: new TabBarView(
        controller: controlador,
        children: <Widget>[
          new VotoDirecto(),
          new AreaOpinionPersonal()
        ],
      ),
    );
  }
}

El contenido de lib/pestanyas/votodirecto.dart:

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 contenido de lib/pestanyas/opinionpersonal.dart:

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"),
          ],
        )));
  }
}

El archivo fuente main.dart es el que nos da las claves para entender como implementar una GUI con un patrón de navegación basado en pestañas. A continuación, vamos a explicar sus partes relevantes.

Como comentamos anteriormente, es la primera vez en el curso de Flutter que construimos un paquete en el que albergar diferentes archivos fuente con clases y widgets de Dart. Así pues, lo primero que hacemos en lib/main.dart es realizar las importaciones de archivos fuentes necesarias para poder tener acceso a estos recursos. La sintaxis es la siguiente:

import 'package:nombre_aplicacion/nombre_paquete/archivo_fuente.dart';

Importamos los archivos fuente que hemos denominado votodirecto.dart y opinionpersonal.dart y que contienen los widgets para cada pestaña. Tras eso, construimos un widget de clase stateful (GestorTabs) en cuyo método build() se despliega la estructura jerárquica de widgets necesaria para incluir el TabBar; los  widgets Tab, que representan cada pestaña; así como el TabBarView que albergará los widgets VotoDirectoy AreaOpinionPersonal(contenido por las pestañas).

GestorTabs es contenido en la ruta inicial de una aplicación Material Design. Vamos a centrarnos por ahora en el método build() de su objeto State (_GestorTabsState). La barra de pestañas (TabBar) requiere un Scaffold y "ser envuelta" por la propiedad nombrada bottom de un AppBar. Como podéis comprobar, el TabBarrecoge una lista de widgets de tipo Tab,  cada uno, en este caso particular sólo contiene un icono acorde al ejemplo.

También es necesario definir un controlador (TabController) que, como su nombre indica, podrá realizar determinadas acciones y controlar propiedades de los widgets TabBar y TabBarView. Podéis ver que dicho controlador se inicializa dentro del método sobreescrito initState() de _GestorTabsState, indicando el número de pestañas que se emplearán y también haciendo uso de la propiedad vsync. Esta última propiedad proviene de la clase SingleTickerProviderStateMixin que es agregada al _GestorTabsState por medio de un mixin. Si queréis saber más sobre los mixins y como se utilizan en Dart os recomiendo visitar este enlace. La presencia de vsync evita que las animaciones fuera de pantalla consuman recursos innecesarios. Se puede usar un objeto de clase StatefulWidget como vsync añadiendo SingleTickerProviderStateMixin a la definición de la clase como hacemos en este ejemplo.

Para terminar de implementar la navegación por pestañas tenemos que agregar el contenido de éstas. Esta labor se consigue implementando un widget TabBarView en la propiedad body del Scaffold. Como podéis comprobar, el TabBarView también requiere el controlador (previamente creado) y, además, tiene una propiedad children que acepta una lista de los widgets que representan el contenido de cada pestaña (VotoDirecto y AreaOpinionPersonal).

Por último, podemos hacer una mejora a este ejemplo. Si lo ejecutáis en el emulador o en una terminal podéis comprobar que cuando abandonamos la pestaña de "voto directo" y más tarde regresamos a ella, los contadores de votos se reinician a cero con el redibujado de los widgets. Podemos modificar este comportamiento fácilmente agregando el mixin AutomaticKeepAliveClientMixin<MiCardStateful> a _MiCardStatefulState (estado del widget MiCardStateful en el archivo fuente lib/pestanyas/votodirecto.dart). Además debemos sobreescribir el método wantKeepAlive() agregado por el mixin para que devuelva true. Con estos sencillos pasos nuestros contadores mantendrán su valor al abandonar la pestaña y posteriormente regresar a ella. Os adjuntamos de nuevo el código de lib/pestanyas/votodirecto.dart con estas modificaciones:

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> 
 with AutomaticKeepAliveClientMixin<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')
                ],
              ))),
    );
  }
  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

En la siguiente lección de este curso de Flutter realizaremos una modificación de esta aplicación de ejemplo para cambiar la barra de pestañas en el AppBar por una barra de navegación inferior desplegada en la propiedad bottomNavigationBar de un widget Scaffold.

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