Añade Interactividad a Tu App Flutter

Lo que aprenderás:

  • Como responder a gestos tap.
  • Como crear un widget personalizado.
  • La diferencia entre stateless y stateful widgets.

¿Cómo modificas tu app para hacer que reaccione a las entradas del usuario? En este tutorial, añadirás interactividad a una app que contiene solo widgets no interactivos. Específicamente, modificarás un icono para hacerlo pulsable creando un widget stateful personalizado que administra dos widgets stateless.

Contenidos

Preparándose

Si ya has contruido el layout en Construyendo Layouts en Flutter, salta a la siguiente sección.

Cuando tienes un dispositivo conectado y habilitado, o has lanzado el simulador iOS (parte de la instalación de Flutter), ¡estás preparado para seguir!

Construyendo Layouts en Flutter mostró como crear el layout de la siguiente captura de pantalla.

La app de inicio Lakes que modificaremos

Cuando la app se lanza por primera vez, la estrella esta rellena de rojo, indicando que este lago ha sido marcado previamente como favorito. El número a continuación de la estrella indica que 41 personas han marcado como favorito este lago. Después de completar este tutorial, pulsando la estrella se desmarcará como favorito, reemplazando la estrella rellena con una sin relleno y decrecerá el contador. Pulsando de nuevo se marca el lago como favorito, dibujando la estrella rellena e incrementando el contador.

el widget custom que crearás

Para lograr esto, crearás un widget personalizado que incluye tanto la estrella como el contador, que son a su vez widgets. Como pulsar sobre la estrella cambia el estado de ambos widgets, entonces el mismo debería administrar ambos.

Puedes empezar a tocar el código en Paso 2: Subclase StatefulWidget. Si quieres probar diferentes maneras de administrar el estado, salta a Administrando el estado.

Widgets Stateful y stateless

¿Qué aprenderás?

  • Algunos widgets son stateful, y otros son stateless.
  • Si un widget cambia—por ejemplo, cuando el usuario interactúa con el—es stateful.
  • El estado de un widget consiste en valores que pueden cambiar, como el valor actual de un slider o cuando un checkbox esta marcado como checked.
  • El estado de un widget es almacenado en un objeto State, separando el estado del widget de su apariencia.
  • Cuando el estado de un widget cambia, el objeto state llama a setState(), diciendo al framework que repinte el widget.

Un widget stateless no tiene estado interno que administrar. Icon, IconButton, y Text son ejemplos de widgets stateless, los cuales son subclases de StatelessWidget.

Un widget stateful es dinámico. El usuario puede interactuar con un stateful widget (escribiendo en un formulario, o moviendo un slider, por ejemplo), o este cambia a lo largo del tiempo (por ejemplo la obtención de datos que causa que se actualice la UI). Checkbox, Radio, Slider, InkWell, Form, and TextField son ejemplos de widgets stateful, los cuales son subclases de StatefulWidget.

Creando un widget stateful

¿Qué aprenderás?

  • Para crear un widget stateful, se hereda de dos clases: StatefulWidget y State.
  • El objeto state contiene el estado del widget y el método build().
  • Cuando el estado del widget cambia, el objeto state llama a setState(), diciéndole al framework que redibuje el widget.

En esta sección, crearás un widget stateful personalizado. Reemplazarás dos widgets stateless—la estrella rellena de rojo y el contador numérico junto a ella—con un único stateful widget personalizado que administra una fila con dos widgets hijos: un IconButton y un Text.

Implementar un widget stateful personalizado requiere crear dos clases:

  • Una subclase de StatefulWidget que define el widget.
  • Una subclase de State que contiene el estado para el widget y define el método build().

Esta sección muestra como construir un StatefulWidget, llamado FavoriteWidget, para la app Lakes. El primer paso es elegir como administrar el estado de FavoriteWidget.

Paso 1: Decide cual objeto administra el estado del widget

El estado de un widget puede ser administrado de varias maneras, pero en nuestro ejemplo, el widget FavoriteWidget, administrará por si mismo su estado. En este ejemplo, alternar la estrella es una acción aislada que no afecta al widget padre o al resto de la UI, por eso el widget puede manejar su estado internamente.

Aprende mas acerca de la separación de widget y estado, y como puede ser admnistrado el estado, en Administrando el estado.

Paso 2: Subclase StatefulWidget

La clase FavoriteWidget administra su propio estado, sobreescribiendo el método createState() para crear el objeto State. El framework llama a createState() cuando quiere construir el widget. En este ejemplo, createState() crea una instancia de _FavoriteWidgetState, que implementarás en el siguiente paso.

class FavoriteWidget extends StatefulWidget {
  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

Paso 3: Subclase State

La clase State personalizada, almacena la información mutable—la lógica y estado interno que puede cambiar durante el tiempo de vida del widget. Cuando la app se lanza por primera vez, la UI muestra una estrella rellena de rojo, indicando que el lago tiene el estado “favorite”, y tiene 41 “likes”. El objeto state almacena esta información en las variables _isFavorited y _favoriteCount.

El objeto state también define el método build. Este método build crea una fila conteniendo un IconButton rojo, y un Text. El widget usa IconButton, (en lugar de Icon), porque este tiene una propiedad onPressed que define el método callback para manejar un gesto tap. IconButton tambien tiene una propiedad icon que guarda el Icon.

El método _toggleFavorite(), que es llamado couando el IconButton es presionado, llama a setState(). Llamar a setState() es fundamental, porque esto dice al framework que el estado del widget ha cambiado y el widget deberia redibujarse. La función _toggleFavorite alterna la UI entre 1) un icono star y el número ‘41’, y 2) un icono star_border y el número ‘40’.

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

  void _toggleFavorite() {
    setState(() {
      // Si el lago esta marcado actualmente como favorito, lo desmarca como favorito.
      if (_isFavorited) {
        _favoriteCount -= 1;
        _isFavorited = false;
        // En otro caso, lo marca como favorito.
      } else {
        _favoriteCount += 1;
        _isFavorited = true;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: EdgeInsets.all(0.0),
          child: IconButton(
            icon: (_isFavorited
                ? Icon(Icons.star)
                : Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(
          width: 18.0,
          child: Container(
            child: Text('$_favoriteCount'),
          ),
        ),
      ],
    );
  }
}

Paso 4: Enchufa el widget stateful en el árbol de widgets

Añade tu widget stateful personalizado al árbol de widgets en el método build de la app. Primero, localiza el código que creaba el Icon y el Text, y borralo:

// ...
Icon(
  Icons.star,
  color: Colors.red[500],
),
Text('41')
// ...

En el mismo sitio, crea el widget stateful:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget titleSection = Container(
      // ...
      child: Row(
        children: [
          Expanded(
            child: Column(
              // ...
          ),
          FavoriteWidget(),
        ],
      ),
    );

    return MaterialApp(
      // ...
    );
  }
}


¡Esto es todo! Cuando haces hot reload de la app, el icono estrella debería responder a gestos tap.

¿Problemas?

Si no puedes hacer que el código se ejecute, mira en tu IDE por posibles errores. Depurar Apps en Flutter puede ayudar. Si sigues sin encontrar el problema, comprueba tu código con el ejemplo “interactive Lakes” en GitHub.

Si sigues teniendo preguntas, acude a Obtener soporte.


El resto de esta página cubre varias maneras en que puede ser administrado el estado de un widget, y lista otros widgets interactivos disponibles.

Administrar el estado

¿Qué aprenderás?

  • Hay diferentes enfoques para administrar estados.
  • Tú, como diseñador del widget, eliges que enfoque usar.
  • Si dudas, empieza administrando el estado en el widget padre.

¿Quién administra el estado de un widget stateful? ¿El propio widget? ¿El widget padre? ¿Ambos? ¿Otro objeto? La respuesta es… depende. Hay muchas formas válidas de hacer tu widget interactivo. Tú, como diseñador del widget, tomas la decisión basándote en como esperas que tu widget sea usado. Aqui están las maneras más comunes de administrar estados:

¿Cómo decides que enfoque usar? Los siguientes principios deberían ayudarte a decidirte:

  • Si el estado en cuestión son datos que genera el usuario, por ejemplo el modo checked o unchecked de un checkbox, o la posición de un slider, entonces es mejor administrar el estado en el widget padre.

  • Si el estado en cuestión es estético, por ejemplo una animación, entonces el estado es mejor administrarlo en el propio widget.

Si dudas, empieza administrando el estado en el widget padre.

Vamos a dar ejemplos de las diferentes maneras de administrar el estado creando tres ejemplos simples: TapboxA, TapboxB, y TapboxC. Todos los ejemplos funcionan de forma similar—cada uno crea un container que, cuando es pulsado, alterna entre una caja verde o gris. El boolean _active determina el color: verede para activo o gris para inactivo.

una caja verde grande con el texto, 'Active'      una caja grande gris con el texto, 'Inactive'

Estos ejemplos usan GestureDetector para capturar la actividad en el Container.

El widget maneja su propio estado

A veces tiene más sentido que el widget administre su estado internamente. Por ejemplo, ListView hace automáticamente scroll cuando su contenido excede la render box. La mayoría de desarrolladores que usan ListView no tienen que administrar el compartamiento de scroll del ListView, ya que ListView administra por si mismo su scroll offset.

La clase _TapboxAState:

  • Administra el estado para TapboxA.
  • Define la propiedad boolean _active que determina el color actual de la caja.
  • Define la función _handleTap(), que actualiza _active cuando la caja es pulsada y llama la función setState() para actualizar el UI.
  • Implementa todo el comportamiento interactivo para el widget.
// TapboxA administra su propio estado.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

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

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}

El widget padre administra el estado del widget

A menudo tiene mas sentido que el widget padre administre el estado y le diga al widget hijo cuando actualizarse. Por ejemplo, IconButton te permite tratar un icono como un botón pulsable. IconButton es un widget stateless porque decidimos que el widget padre necesita saber cuando se ha pulsado el botón, para entonces, poder tomar la acción adecuada.

En el siguiente ejemplo, TapboxB exporta su estado a su padre a través de un callback. Como TapboxB no administra su estado, es una subclase de StatelessWidget.

La clase ParentWidgetState:

  • Administra el estado _active para TapboxB.
  • Implementa _handleTapboxChanged(), el método llamado cuando la caja es pulsada.
  • Cuando el estado cambia, llama a setState() para actualizar el UI.

La clase TapboxB:

  • Hereda de StatelessWidget porque todo estado es manejado por su padre.
  • Cuando detecta un gesto tap, notifica al padre.
// ParentWidget administra el estado para TapboxB.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

Un enfoque intermedio

Para algunos widgets, un enfoque intermedio es más conveniente. En este escenario, el widget stateful administra algo del estado, y el widget padre administra otro aspecto del estado.

En el ejemplo TapboxC, cuando el gesto tap inicia, tap down, un borde verde oscuro aparece alrededor de la caja. Cuando el gesto tap acaba, tap up, el borde desaparece y el color de la caja cambia. TapboxC exporta su estado _active a su padre pero administra internamente su estado _highlight. Este ejemplo tiene dos objetos State, _ParentWidgetState y _TapboxCState.

El objeto _ParentWidgetState:

  • Administra el estado _active.
  • Implementa _handleTapboxChanged(), el método que se llama cuando la caja es pulsada.
  • Llama a setState() para actualizar el UI cuando un gesto tap ocurre y el estado _active cambia.

El objeto _TapboxCState:

  • Administra el estado _highlight.
  • El GestureDetector escucha todos los eventos tap. Cuando el usaurio hace tap down, añade el resaltado (implementado como un borde verde oscuro). Cuando el usuario termina de pulsar, tap up, el resaltado se elimina.
  • LLama a setState() para actualizar el UI cuando se hace tap down, tap up, o se cancela el tap, y cambie el estado _highlight.
  • Cuan hay un evento tap, pasa este cambio de estado al widget padre para tomar la acción adecuada usando la propiedad widget.
//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  TapboxC({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  _TapboxCState createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  Widget build(BuildContext context) {
    // Este ejemplo añade un borde verde cuando se produce el evento "tap down".
    // En el evento "tap up", el cuadrado cambia al estado opuesto.
    return GestureDetector(
      onTapDown: _handleTapDown, // Maneja los eventos tap en el orden que 
      onTapUp: _handleTapUp, // estos ocurren: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color:
              widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

Una implementación alternativa podría haber sido exportar el estado resaltado al padre mientras mantiene el estado active internamente, pero si preguntas a alguien por usar este tap box, probablemente opinen que no tienen mucho sentido. Al desarrollador le importa si la caja esta activa. Al desarrollador probablemente no le preocupe como se administra el resaltado, y prefiera que el tap box maneje estos detalles.


Otros widgets interactivos

Flutter ofrece una variedad de botones y widgets interactivos similares. La mayoría de estos widgets implementan las Material Design guidelines, que definen un conjunto de componentes con una UI pragmática.

Si lo prefieres, puedes usar GestureDetector para construir interactividad en cualquier widget personalizado. Puedes encontrar ejemplos de GestureDetector en Administrado el estado, y en la Flutter Gallery.

Cuando necesitas interactividad, lo más sencillo es usar uno de los widgets prefabricados. Aquí está una lista parcial:

Widgets standard:

Material Components:

Recursos

Los siguientes recursos pueden ayudar cuando añades interactividad a tu app.