Saltar al contenido principal

Agrega interactividad a tu app de Flutter

Cómo implementar un stateful widget que responde a toques.

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

El tutorial para construir layouts te mostró cómo crear el layout para la siguiente captura de pantalla.

La app del tutorial de layout

La app del tutorial de layout

Cuando la app se inicia por primera vez, la estrella es de color rojo sólido, lo que indica que este lago ha sido marcado como favorito previamente. El número junto a la estrella indica que 41 personas han marcado como favorito este lago. Después de completar este tutorial, tocar la estrella elimina su estado de favorito, reemplazando la estrella sólida con un contorno y disminuyendo el contador. Tocar de nuevo marca como favorito el lago, dibujando una estrella sólida e incrementando el contador.

El widget personalizado que crearás

Para lograr esto, crearás un único widget personalizado que incluye tanto la estrella como el contador, que son widgets en sí mismos. Tocar la estrella cambia el State de ambos widgets, por lo que el mismo widget debería gestionar ambos.

Puedes ir directamente a tocar el código en Paso 2: Hereda de StatefulWidget. Si quieres probar diferentes formas de gestionar el State, salta a Gestionando el State.

Widgets Stateful y stateless

#

Un widget es o bien stateful o bien stateless. Si un widget puede cambiar—cuando un usuario interactúa con él, por ejemplo—es stateful.

Un widget stateless nunca cambia. Icon, IconButton, y Text son ejemplos de widgets stateless. Los widgets stateless heredan de StatelessWidget.

Un widget stateful es dinámico: por ejemplo, puede cambiar su apariencia en respuesta a eventos desencadenados por interacciones del usuario o cuando recibe datos. Checkbox, Radio, Slider, InkWell, Form, y TextField son ejemplos de widgets stateful. Los widgets stateful heredan de StatefulWidget.

El State de un widget se almacena en un objeto State, separando el State del widget de su apariencia. El State consiste en valores que pueden cambiar, como el valor actual de un slider o si un checkbox está marcado. Cuando el State del widget cambia, el objeto State llama a setState(), diciéndole al framework que redibuje el widget.

Creando un stateful widget

#

En esta sección, crearás un stateful widget personalizado. Reemplazarás dos widgets stateless—la estrella roja sólida y el contador numérico junto a ella—con un solo stateful widget personalizado que gestiona una fila con dos widgets hijos: un IconButton y un Text.

Implementar un stateful widget personalizado requiere crear dos clases:

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

Esta sección te muestra cómo construir un stateful widget, llamado FavoriteWidget, para la app de lagos. Después de configurar, tu primer paso es elegir cómo se gestiona el State para FavoriteWidget.

Paso 0: Prepárate

#

Si ya construiste la app en el tutorial para construir layouts, salta a la siguiente sección.

  1. Asegúrate de haber configurado tu entorno.
  2. Crea una nueva aplicación de Flutter.
  3. Reemplaza el archivo lib/main.dart con main.dart.
  4. Reemplaza el archivo pubspec.yaml con pubspec.yaml.
  5. Crea un directorio images en tu proyecto, y agrega lake.jpg.

Una vez que tengas un dispositivo conectado y habilitado, o hayas iniciado el simulador de iOS (parte de la instalación de Flutter) o el emulador de Android (parte de la instalación de Android Studio), ¡estarás listo para comenzar!

Paso 1: Decide qué objeto gestiona el State del widget

#

El State de un widget se puede gestionar de varias maneras, pero en nuestro ejemplo el propio widget, FavoriteWidget, gestionará su propio State. En este ejemplo, alternar la estrella es una acción aislada que no afecta al widget padre ni al resto de la UI, por lo que el widget puede manejar su State internamente.

Aprende más sobre la separación de widget y State, y cómo se podría gestionar el State, en Gestionando el State.

Paso 2: Hereda de StatefulWidget

#

La clase FavoriteWidget gestiona su propio State, por lo que sobrescribe createState() para crear un objeto State. El framework llama a createState() cuando quiere construir el widget. En este ejemplo, createState() devuelve una instancia de _FavoriteWidgetState, que implementarás en el siguiente paso.

dart
class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({super.key});

  @override
  State<FavoriteWidget> createState() => _FavoriteWidgetState();
}

Paso 3: Hereda de State

#

La clase _FavoriteWidgetState almacena los datos mutables que pueden cambiar a lo largo de la vida útil del widget. Cuando la app se inicia por primera vez, la UI muestra una estrella roja sólida, indicando que el lago tiene estado "favorito", junto con 41 me gusta. Estos valores se almacenan en los campos _isFavorited y _favoriteCount:

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

La clase también define un método build(), que crea una fila que contiene un IconButton rojo, y Text. Usas IconButton (en lugar de Icon) porque tiene una propiedad onPressed que define la función callback (_toggleFavorite) para manejar un toque. Definirás la función callback a continuación.

dart
class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.center,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(width: 18, child: SizedBox(child: Text('$_favoriteCount'))),
      ],
    );
  }

  // ···
}

El método _toggleFavorite(), que se llama cuando se presiona el IconButton, llama a setState(). Llamar a setState() es crítico, porque esto le dice al framework que el State del widget ha cambiado y que el widget debe ser redibujado. El argumento de la función para setState() alterna la UI entre estos dos estados:

  • Un icono de estrella y el número 41
  • Un icono star_border y el número 40
dart
void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

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

#

Agrega tu stateful widget personalizado al árbol de widgets en el método build() de la app. Primero, localiza el código que crea el Icon y el Text, y elimínalo. En la misma ubicación, crea el stateful widget:

dart
child: Row(
  children: [
    // ...
    Icon(
      Icons.star,
      color: Colors.red[500],
    ),
    const Text('41'),
    const FavoriteWidget(),
  ],
),

¡Eso es todo! Cuando realices un Hot Reload de la app, el icono de estrella debería responder ahora a los toques.

¿Problemas?

#

Si no consigues ejecutar tu código, busca en tu IDE posibles errores. Depurar apps de Flutter podría ayudar. Si aún no encuentras el problema, compara tu código con el ejemplo interactivo de lagos en GitHub.

Si aún tienes preguntas, consulta cualquiera de los canales de la comunidad de desarrolladores.


El resto de esta página cubre varias formas en que se puede gestionar el State de un widget, y enumera otros widgets interactivos disponibles.

Gestionar el state

#

¿Quién gestiona el State del stateful widget? ¿El widget mismo? ¿El widget padre? ¿Ambos? ¿Otro objeto? La respuesta es... depende. Hay varias formas válidas de hacer que tu widget sea interactivo. Tú, como diseñador del widget, tomas la decisión en función de cómo esperas que se use tu widget. Aquí están las formas más comunes de gestionar el State:

¿Cómo decides qué enfoque utilizar? Los siguientes principios deberían ayudarte a decidir:

  • Si el State en cuestión son datos del usuario, por ejemplo el modo marcado o desmarcado de un checkbox, o la posición de un slider, entonces es mejor que el State sea gestionado por el widget padre.

  • Si el State en cuestión es estético, por ejemplo una animación, entonces es mejor que el State sea gestionado por el propio widget.

En caso de duda, comienza gestionando el State en el widget padre.

Daremos ejemplos de las diferentes formas de gestionar el State creando tres ejemplos sencillos: TapboxA, TapboxB, y TapboxC. Todos los ejemplos funcionan de manera similar—cada uno crea un contenedor que, al tocarlo, alterna entre una caja verde o gris. El booleano _active determina el color: verde para activo o gris para inactivo.

Estado activo Estado inactivo

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

El widget gestiona su propio state

#

A veces tiene más sentido que el widget gestione su State internamente. Por ejemplo, ListView se desplaza automáticamente cuando su contenido supera la caja de renderizado. La mayoría de los desarrolladores que usan ListView no quieren gestionar el comportamiento de desplazamiento de ListView, por lo que ListView en sí gestiona su offset de desplazamiento.

La clase _TapboxAState:

  • Gestiona el State para TapboxA.
  • Define el booleano _active que determina el color actual de la caja.
  • Define la función _handleTap(), que actualiza _active cuando se toca la caja y llama a la función setState() para actualizar la UI.
  • Implementa todo el comportamiento interactivo para el widget.
dart
import 'package:flutter/material.dart';

// TapboxA manages its own state.

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

class TapboxA extends StatefulWidget {
  const TapboxA({super.key});

  @override
  State<TapboxA> createState() => _TapboxAState();
}

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

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

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

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

El widget padre gestiona el state del widget

#

A menudo tiene más sentido que el widget padre gestione el State y le diga a su widget hijo cuándo actualizarse. Por ejemplo, IconButton te permite tratar un icono como un botón tocable. IconButton es un stateless widget porque decidimos que el widget padre necesita saber si el botón ha sido tocado, para poder tomar las medidas adecuadas.

En el siguiente ejemplo, TapboxB exporta su State a su padre a través de un callback. Debido a que TapboxB no gestiona ningún State, hereda de StatelessWidget.

La clase ParentWidgetState:

  • Gestiona el State _active para TapboxB.
  • Implementa _handleTapboxChanged(), el método llamado cuando se toca la caja.
  • Cuando el State cambia, llama a setState() para actualizar la UI.

La clase TapboxB:

  • Extiende StatelessWidget porque todo el State es manejado por su padre.
  • Cuando se detecta un toque, notifica al padre.
dart
import 'package:flutter/material.dart';

// ParentWidget manages the state for TapboxB.

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

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

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

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

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

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

class TapboxB extends StatelessWidget {
  const TapboxB({super.key, this.active = false, required this.onChanged});

  final bool active;
  final ValueChanged<bool> onChanged;

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

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

Un enfoque híbrido (mix-and-match)

#

Para algunos widgets, un enfoque de combinación tiene más sentido. En este escenario, el stateful widget gestiona parte del State, y el widget padre gestiona otros aspectos del State.

En el ejemplo de TapboxC, al presionar, aparece un borde verde oscuro alrededor de la caja. Al soltar, el borde desaparece y el color de la caja cambia. TapboxC exporta su State _active a su padre pero gestiona su State _highlight internamente. Este ejemplo tiene dos objetos State, _ParentWidgetState y _TapboxCState.

El objeto _ParentWidgetState:

  • Gestiona el State _active.
  • Implementa _handleTapboxChanged(), el método llamado cuando se toca la caja.
  • Llama a setState() para actualizar la UI cuando ocurre un toque y el State _active cambia.

El objeto _TapboxCState:

  • Gestiona el State _highlight.
  • El GestureDetector escucha todos los eventos de toque. A medida que el usuario presiona, agrega el resaltado (implementado como un borde verde oscuro). A medida que el usuario suelta el toque, elimina el resaltado.
  • Llama a setState() para actualizar la UI al presionar, soltar o cancelar el toque, y el State _highlight cambia.
  • En un evento de toque, pasa ese cambio de State al widget padre para que tome la acción adecuada utilizando la propiedad widget.
dart
import 'package:flutter/material.dart';

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

class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

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

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

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

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

class TapboxC extends StatefulWidget {
  const TapboxC({super.key, this.active = false, required this.onChanged});

  final bool active;
  final ValueChanged<bool> onChanged;

  @override
  State<TapboxC> 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);
  }

  @override
  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        width: 200,
        height: 200,
        decoration: BoxDecoration(
          color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(color: Colors.teal[700]!, width: 10)
              : null,
        ),
        child: Center(
          child: Text(
            widget.active ? 'Active' : 'Inactive',
            style: const TextStyle(fontSize: 32, color: Colors.white),
          ),
        ),
      ),
    );
  }
}

Una implementación alternativa podría haber exportado el State de resaltado al padre mientras mantenía el State activo interno, pero si le pidieras a alguien que use esa caja de toques, probablemente se quejaría de que no tiene mucho sentido. Al desarrollador le importa si la caja está activa. Al desarrollador probablemente no le importa cómo se gestiona el resaltado, y prefiere que la caja de toques maneje esos detalles.


Otros widgets interactivos

#

Flutter ofrece una variedad de botones y widgets interactivos similares. La mayoría de estos widgets implementan las guías de Material Design, que definen un conjunto de componentes con una UI con opinión propia.

Si lo prefieres, puedes usar GestureDetector para incorporar interactividad en cualquier widget personalizado. Puedes encontrar ejemplos de GestureDetector en Gestionando el State. Aprende más sobre el GestureDetector en Manejar toques, una receta en el libro de recetas de Flutter.

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

Widgets estándar

#

Material Components

#

Recursos

#

Los siguientes recursos pueden ser de ayuda al agregar interactividad a tu app.

Gestos, una sección en el libro de recetas de Flutter.

Manejo de gestos

Cómo crear un botón y hacer que responda a las entradas.

Gestos en Flutter

Una descripción del mecanismo de gestos de Flutter.

Documentación de la API de Flutter

Documentación de referencia para todas las librerías de Flutter.

Aplicación Wonderous aplicación en ejecución , repo

App de demostración de Flutter con un diseño personalizado e interacciones atractivas.

Diseño por capas de Flutter (video)

Este video incluye información sobre widgets stateful y stateless. Presentado por el ingeniero de Google, Ian Hickson.