FutureBuilder & StreamBuilder: buenas prácticas en Flutter con Dart

FutureBuilder & StreamBuilder: buenas prácticas en Flutter con Dart

Si desarrollas apps Flutter, seguramente te has topado con los famosos FutureBuilder y StreamBuilder. Son geniales para mostrar datos asíncronos sin complicarte, pero pueden convertirse rápidamente en fuente de frustración si se usan mal.

En este artículo, repasaremos las buenas prácticas para evitar crashes y bugs extraños. Spoiler: verás que con los records de Dart (introducidos en Dart 3), ¡simplificamos aún más el código!

¿Qué son FutureBuilder y StreamBuilder ?

FutureBuilder

Un FutureBuilder te permite lanzar un proceso asíncrono —vía un Future— y reconstruir automáticamente la UI cuando el resultado está disponible. Por ejemplo, si obtienes un nombre de usuario desde una API:

FutureBuilder<String>(
  future: getUserName(), // getUserName() -> Future<String>
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Erreur : ${snapshot.error}');
    }
    return Text('Nom : ${snapshot.data}');
  },
)

El principio es simple: proporcionas tu future, y el widget se actualiza cuando recibe la respuesta.

StreamBuilder

Para datos que evolucionan de forma continua, el StreamBuilder es tu mejor amigo. Imagina un contador que se incrementa cada segundo, o una serie de mensajes que llegan poco a poco.

StreamBuilder<int>(
  stream: counterStream(), // counterStream() -> Stream<int>
  builder: (context, snapshot) {
    // L'état active signifie qu'on a commencé à recevoir des données du Stream.
    if (snapshot.connectionState == ConnectionState.active) {
      return Text('Compteur : ${snapshot.data}');
    } else if (snapshot.connectionState == ConnectionState.waiting) {
      // waiting indique qu'on attend encore les premiers events
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('Erreur : ${snapshot.error}');
    } else if (snapshot.connectionState == ConnectionState.done) {
      // quand le flux est terminé
      return Text('Le stream est terminé. Dernière valeur : ${snapshot.data}');
    } else {
      return SizedBox.shrink();
    }
  },
)

Cada nuevo evento en el Stream vuelve a construir tu widget. Mágico. Aquí distinguimos varios estados:

  • waiting : el stream está creado pero aún no ha emitido datos,
  • active : efectivamente estamos recibiendo datos,
  • done : el flujo está cerrado.

Inferencia de tipos con Record

Hay una pequeña revolución en Dart 3: ¡los records! Un record es un mini-objeto que puede agregar varios valores de tipos variados, un poco como una Tuple en otros lenguajes. Gracias a esto, evitamos andar con List<dynamic> por todas partes y ganamos claridad.

¿Por qué es útil?

Tomemos un ejemplo concreto. Supongamos que quieres recuperar, en una sola petición, el alias y la edad de un usuario. En lugar de devolver una lista (lo cual puede ser ambiguo: ["Martin", 42]… eh, ¿en qué orden era?), Dart 3 te permite devolver directamente un record. Ejemplo: (String, int).

Ejemplos de código

Antes (con List<dynamic>)

// Simule une fonction qui renvoie deux infos : username et age
Future<List<dynamic>> fetchData() async {
  await Future.delayed(Duration(seconds: 1));
  return ["Martin", 42];
}

FutureBuilder<List<dynamic>>(
  future: fetchData(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      final data = snapshot.data!;
      final username = data[0] as String;
      final age = data[1] as int;
      return Text('$username, $age');
    }
    return CircularProgressIndicator();
  },
)
  • Problema: hay que castear (as String, as int), se maneja un List<dynamic>… En resumen, no es lo ideal.

Después (con un Record)

// Même fonction, mais on renvoie un record plus explicite
Future<(String, int)> fetchData() async {
  await Future.delayed(Duration(seconds: 1));
  return ("Martin", 42);
}

FutureBuilder<(String, int)>(
  future: fetchData(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Erreur : ${snapshot.error}');
    }

    if (snapshot.hasData) {
      final (username, age) = snapshot.data!;
      return Text('$username, $age');
    }
    return SizedBox.shrink(); // cas par défaut
  },
)
  • Aquí ya no hace falta castear: Dart sabe que tenemos (String, int).
  • El código es más legible y reducimos drásticamente el riesgo de error.

Atención: es mejor evitar llamar a fetchData() directamente en el FutureBuilder si no quieres relanzarlo en bucle. Hablaremos de ello un poco más abajo.

Errores frecuentes a evitar

Abuso de FutureBuilder y StreamBuilder

El gran error clásico: poner future: fetchData() o stream: getStream() dentro del método build(). ¿Resultado? A cada rebuild relanzas tu petición (y lloras). ¿La solución? Instancia tu Future o tu Stream una sola vez.

Uso incorrecto

FutureBuilder(
  future: fetchData(),
  builder: (context, snapshot) {
    // ...
  },
)

Mejor enfoque

late final Future<(String, int)> future;

@override
void initState() {
  super.initState();
  future = fetchData(); // un solo llamado
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<(String, int)>(
    future: future,
    builder: (context, snapshot) {
      // ...
    },
  );
}

Eso te evita sorpresas y llamadas innecesarias.

Consejo de rendimiento : si tu resultado no cambia a menudo, piensa también en implementar un sistema de cache o comprobar si tus datos ya están cargados antes de relanzar la petición. Eso te evitará sobrecargar la red y volver a renderizar con demasiada frecuencia.

Gestión de errores

En caso de error (snapshot.hasError), puedes mostrar un mensaje dedicado, incluso ofrecer un botón "Reintentar" para relanzar la petición. Ejemplo simplificado:

if (snapshot.hasError) {
  return Column(
    children: [
      Text('Oups ! Erreur : \\${snapshot.error}'),
      ElevatedButton(
        onPressed: () {
          setState(() {
            // On relance le Future ou on appelle un autre mécanisme
            // si on l'a stocké dans future = fetchData() etc.
            future = fetchData();
          });
        },
        child: Text('Réessayer'),
      ),
    ],
  );
}

Así, el usuario no queda bloqueado y puede repetir la acción.

Ejemplo de uso concreto de un Stream

Para ir más lejos, tomemos un escenario de mensajería instantánea. Podríamos imaginar un Stream<String> que emite nuevos mensajes a medida que llegan:

StreamBuilder<String>(
  stream: chatStream(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Text('Chargement du chat...');
    } else if (snapshot.hasError) {
      return Text('Erreur : ${snapshot.error}');
    } else if (snapshot.hasData) {
      final newMessage = snapshot.data!;
      return Text('Nouveau message : $newMessage');
    } else {
      return Text('Aucun message');
    }
  },
)

Aquí, cada nuevo mensaje "push" en chatStream() dispara un rebuild. Ideal para un chat en tiempo real o cualquier flujo continuo de eventos.

Conclusión

Y ya está, tienes una visión de las buenas prácticas alrededor de FutureBuilder y StreamBuilder. Con Dart 3, los records hacen el código más limpio, más robusto y más fácil de mantener. Recuerda simplemente:

  1. No desencadenar tus futures/streams dentro de build().
  2. Manejar bien todos los estados (loading, error, done, etc.).
  3. Probar los records para reducir el uso de List<dynamic>.
  4. Tener en cuenta el rendimiento (no relanzar innecesariamente, pensar en el cache)
  5. Ofrecer una forma de manejar o relanzar en caso de error.

De verdad, verás que hace el código más agradable y menos propenso a fallos. Para profundizar, echa un vistazo a la documentación oficial de Flutter y a Dart (en particular los records). Allí encontrarás también otros ejemplos y casos de uso.

¿Y tú, cómo gestionas tus llamadas asíncronas? Comparte tus comentarios y consejos en los comentarios, ¡tengo curiosidad por leerlos!

Etiquetas

  • flutter

  • ios

  • android

  • dart

  • mejores-prácticas

Este artículo fue publicado el

Comentarios

Cargando...