latest

Introducción a la programación basada en widgets. Entrada de Texto, Cuadros de Diálogo y Alertas

En esta lección de nuestro curso de Flutter vamos a partir de una variación de la GUI de la clase anterior de introducción a los Stateful Widgets. Podéis ver una captura de pantalla de la nueva interfaz de usuario a continuación.

Básicamente, los principales cambios a nivel visual son:

  • Hemos quitado el StatefulWidget de clase MiCardStateful para el estado de opinión de "Ni fu n fa".
  • En el método build() de _MiCardStatefulState hemos sustituido el widget Column por un widget de clase Row para disponer horizontalmente los widgets que lo componen: Icon, Padding, RaisedButton, Padding, Text.
  • Hemos adaptado los tamaños de los iconos, de las tipografías y reducido los valores de padding.
  • Hemos implementado un nuevo StatefulWidget denominado AreaOpinionPersonal. En el método build() de su State<AreaOpinionPersonal> correspondiente (_AreaOpinionPersonalState), se ha construido un widget Column que contiene 4 elementos: un widget de clase TextField (para entrada de texto), un widget de Padding, un RaisedButton y, por último, un widget Text.
  • El nuevo widget AreaOpinionPersonal sirve para emular un espacio en el que el usuario pueda verter su opinión personal sobre un hipotético curso en formato de texto libre. El usuario redacta su opinión y cuando termina presiona sobre el RaisedButton ("Enviar opinión personal"). Esa acción desplegará un cuadro de diálogo (AlertDialog) como el siguiente:

  • El cuadro de diálogo presenta el texto (opinión) que contenía el widget TextField en el momento de presionar el RaisedButton, con dos opciones de respuesta posibles en forma de widget FlatButton. Si el usuario presiona "No" el cuadro de diálogo se cerrará y no ocurrirá nada más (el foco de interacción volverá al TextField que conservará el texto que había escrito); por el contrario, si presiona , no sólo desaparecerá el cuadro de diálogo, además se desencadenarán una serie de acciones, concretamente:
    • En el widget inferior (de clase Text) se mostrará el texto escrito por el usuario como la "Última opinión enviada".
    • Se limpiará el contenido del TextField para que otro usuario pueda dar su opinión en forma de texto libre.

A continuación os facilitamos el código de este ejemplo en el que combinamos un TextField y un cuadro de diálogo modal AlertDialog. Comentaremos las partes relevantes justo después.

import 'package:flutter/material.dart';

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

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: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                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(15.0), // padding interno del botón
                  ),
                  Padding(
                    padding: EdgeInsets.all(6.0),
                  ),
                  Text(_contador.toString(),
                      style: TextStyle(
                        fontSize: 18.0,
                      )), // Otra manera: Text('$_contador')
                ],
              ))),
    );
  }
}

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

class MiStatelessWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: new AppBar(
          title: new Text("¿Qué os parece el curso?"),
          backgroundColor: Colors.orangeAccent,
        ),
        body: 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),
            Padding(
              padding: const EdgeInsets.only(top: 65.0),
            ),
            new AreaOpinionPersonal(),
          ],
        )));
  }
}

Como ocurría en la interfaz de usuario de la sesión anterior, hemos implementado un StatelessWidget (MiStatelessWidget)  que incorpora un Scaffold con su correspondiente appBar:AppBar, además de su propiedad body. De esta última, parte un widget Column con cuatro widgets hijos: los MiCardStateful, para votar simplemente "Me gusta" o "No me gusta"; un widget Padding para separar visualmente la parte de votos directos del área de opinión personal en formato de texto libre; y por último, el nuevo widget mutable (con estado) AreaOpinionPersonal.

Respecto de los MiCardStateful, no hay mucho que añadir. Se ha sustituido el widget Column por un widget Row, se han reducido los valores de padding y tamaño de letra y se han alineado todos los elementos en el lado izquierdo con mainAxisAlignment: MainAxisAlignment.start.  

La parte más novedosa que incorpora este ejemplo es la que tiene que ver con el widget de AreaOpinionPersonal. Primero exponemos su código y luego vamos a destacar determinadas partes de su implementación.

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

Podemos ver que no difiere en gran medida de la definición de widget con estado que hemos visto con anterioridad. AreaOpinionPersonal tiene asociado una subclase de State (_AreaOpinionPersonalState). En su método build(), implementamos un Container que envuelve un widget Center y éste a su vez un widget Column. De la propiedad children del Column "cuelgan" el widget TextField (el campo de texto) y un widget de Padding que sirve para establecer algo de separación con el RaisedButton que constituye el tercer widget.

Prestad atención y observad que los 3 elementos del Column están configurados para ocupar todo el ancho de la pantalla (propiedad: crossAxisAlignment: CrossAxisAlignment.stretch).

Centrándonos ya en el TextField, la propiedad decoration y el widget InputDecoration nos permiten que el campo de texto tenga un placeholder. Por otro lado, el parámetro onChanged requiere un callback con un String como parámetro ((String data){}). El evento onChanged se lanza cada vez que el contenido del TextField cambia. Así pues, nosotros aprovechamos esta circunstancia y en el callback de respuesta a dicho evento actualizamos la propiedad dinámica String _opinion, definida en _AreaOpinionPersonalState. De esta forma almacenamos la opinión en formato texto en su estado actual. Observad que en este caso no es necesario invocar setState((){}), dado que no tenemos ningún otro elemento en pantalla (previamente renderizado) que deba volver a renderizarse, fruto de modificar la propiedad dinámica _opinion.

La propiedad maxLines:2 se utiliza para indicar que el campo de texto ocupe dos líneas (y no más que dos líneas visibles de texto). Por último, la propiedad controller nos permite definir un TextEditingController (que nosotros hemos denominado opinionController). Este tipo de controlador, como veremos a continuación, nos permite realizar determinadas acciones sobre el TextField, en nuestro caso particular, borrar su contenido.

Continuamos avanzando en el análisis del código, fijando nuestra atención en el RaisedButton encargado de emular que la opinión personal es guardada y/o enviada. Define un onPressed con un VoidCallback que hemos implementado dentro de la clase _AreaOpinionPersonalState y que se denomina _mostrarDialogo(). En el cuerpo de este método se construye el AlertDialog (cuadro de diálogo modal) que hemos denominado dialogo. Con su propiedad content definimos el texto a mostrar (que será el texto contenido previamente por la variable _opinion). La propiedad actions, por su parte, admite una List de Widget. Es utilizada para definir las diferentes acciones, en este caso, empleado un par de widgets de botones de clase FlatButton para proporcionar las elecciones de "Sí" y "No".

Si se presiona el FlatButton "No", simplemente se cerrará el cuadro de diálogo (Navigator.pop(context)) y se imprimirá la negativa correspondiente por consola de texto. Si se presiona el FlatButton "Sí", se imprimirá el mensaje de aceptación y, además, se invocará setState((){}) con un VoidCallback en el que:

  1. Se limpia el contenido del TextField usando el controlador definido previamente (de clase TextEditingController).
  2. Se asigna a la variable (propiedad dinámica) _ultOpinionEnviada el valor actual de opinión (_opinion) y a _opinion una cadena vacía, para que quede lista para albergar nuevas opiniones futuras.
    El hecho de realizar la asignación de _ultOpinionEnviada dentro de un setState((){}) garantiza que el widget Text que muestra dicha variable en el StatefulWidget (AreaOpinionPersonal) va a volver a ser renderizado implícitamente mostrando la "última opinión enviada".

El cuadro de diálogo es mostrado con la ejecución de la sentencia showDialog(context: context, child: dialogo);

Por último, prestad atención a como se ha utilizado una enumeración (enum) con valores "Sí" y "No" para determinar la acción a realizar en el cuadro de diálogo. De igual forma que en este ejemplo hemos construido un cuadro de diálogo modal con dos acciones u opciones de respuesta, podemos construir un cuadro de alerta con una única acción a realizar.

En esta sesión hemos aprendido bastante sobre el uso de TextField como mecanismo de entrada de texto, así como sobre la utilización de cuadros de diálogo modales. Todo ello combinado con los conocimientos previos adquiridos sobre widgets con / sin estado.

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