Start Debugging

FutureBuilder/StreamBuilder vs AsyncValue de Riverpod en Flutter: cual deberias usar?

Usa FutureBuilder o StreamBuilder para un widget asincrono autocontenido y desechable. Recurre a AsyncValue de Riverpod cuando el resultado se comparte, se cachea o se muta. Aqui esta la decision, los puntos delicados y codigo ejecutable para ambos. Probado en Flutter 3.44 y flutter_riverpod 3.3.1.

Si estas decidiendo entre los FutureBuilder / StreamBuilder integrados de Flutter y el AsyncValue de Riverpod, la respuesta corta es: conserva los builders para un widget unico y autocontenido que posee un resultado asincrono desechable, y pasa a AsyncValue de Riverpod en el momento en que ese resultado se comparte entre pantallas, se cachea, se refresca o se muta. Los builders no son “la version para principiantes” de lo mismo. Son una primitiva de UI que se suscribe a un objeto asincrono. AsyncValue es un modelo de estado que vive fuera del arbol de widgets. Esta guia esta probada en Flutter 3.44 (estable, 2026-05-18), Dart 3.12 y flutter_riverpod 3.3.1 (la linea 3.0 salio el 2025-09-10).

Resuelven problemas que se solapan en capas distintas

FutureBuilder y StreamBuilder son widgets. Le pasas a cada uno un Future o Stream, y le entrega a tu callback builder un AsyncSnapshot<T> que describe el estado de conexion actual (waiting, active, done) mas los ultimos datos o el error. El widget se suscribe cuando se inserta, se desuscribe cuando se elimina y se vuelve a suscribir si le pasas una instancia distinta de Future/Stream. Ese es todo el contrato. No hay caching, ni compartir, ni memoria del resultado una vez que el widget sale del arbol.

El AsyncValue<T> de Riverpod no es un widget en absoluto. Es una union sellada con tres subtipos (AsyncData, AsyncLoading, AsyncError) que un provider expone como su valor. El trabajo asincrono se ejecuta dentro de un provider que vive fuera del arbol de widgets, asi que cualquier widget puede leerlo, varios widgets pueden leer la misma instancia y el resultado sobrevive a las reconstrucciones y a la navegacion. Lo renderizas con value.when(...) o un switch de Dart 3, igual que renderizas un AsyncSnapshot, pero la fuente de verdad es un provider en lugar de un campo del widget.

Asi que la verdadera pregunta no es “cual renderiza mejor tres estados”. Ambos renderizan tres estados bien. La pregunta es donde debe vivir el resultado asincrono y cuantas cosas necesitan verlo.

Matriz de caracteristicas

AspectoFutureBuilder / StreamBuilder (Flutter 3.44)AsyncValue de Riverpod (flutter_riverpod 3.3.1)
Que esUn widget que se suscribe a un Future/StreamUn tipo de estado sellado expuesto por un provider
Donde vive el resultadoEn el widget, muere al desmontarse el widgetEn un provider, fuera del arbol, sobrevive a la navegacion
Compartir entre pantallasNo, cada builder reejecuta su propio trabajoSi, un provider leido desde muchos widgets
Caching / dedupNinguno, memoizas el Future tu mismoIntegrado, el provider cachea hasta invalidar
Disparo en cada reconstruccionSi, si el Future se crea en buildNo, el build del provider se ejecuta una vez hasta invalidar
Loading + datos previosManual, el snapshot pierde data mientras esperavalue.isLoading mantiene value durante el refresco
Mutaciones / refrescoReasignar el Future y setStateref.invalidate o AsyncValue.guard en un notifier
Probar sin un widgetDificil, necesita pumpWidgetFacil, lee el provider en un ProviderContainer plano
DependenciasCero, viene con el SDKPaquete flutter_riverpod
Lineas de boilerplate para algo unicoMinimasMas configuracion para una sola llamada desechable

Cuando FutureBuilder o StreamBuilder es la eleccion correcta

Recurre a los builders integrados cuando el resultado asincrono pertenece genuinamente a un widget y nadie mas lo necesita.

Aqui esta la forma correcta. El Future se crea una vez en initState, no en build, asi que el widget no vuelve a hacer fetch en cada reconstruccion del padre.

// Flutter 3.44, Dart 3.12
class UserCard extends StatefulWidget {
  const UserCard({super.key, required this.id});
  final String id;

  @override
  State<UserCard> createState() => _UserCardState();
}

class _UserCardState extends State<UserCard> {
  late Future<User> _user;

  @override
  void initState() {
    super.initState();
    _user = api.fetchUser(widget.id); // created ONCE, not in build
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: _user,
      builder: (context, snapshot) {
        return switch (snapshot) {
          AsyncSnapshot(connectionState: ConnectionState.waiting) =>
            const CircularProgressIndicator(),
          AsyncSnapshot(hasError: true, :final error) =>
            Text('Failed: $error'),
          AsyncSnapshot(hasData: true, :final data?) =>
            Text(data.name),
          _ => const SizedBox.shrink(),
        };
      },
    );
  }
}

El bug mas comun con este widget es crear el Future en linea, como future: api.fetchUser(widget.id) directamente en build. Cada reconstruccion entonces asigna un nuevo Future, FutureBuilder ve una nueva identidad y reinicia desde el estado de loading. Ese modo de fallo es lo bastante comun como para tener su propio articulo: consulta por que FutureBuilder recrea su Future en cada reconstruccion para la reproduccion completa y todas las variantes que lo disparan.

Cuando AsyncValue de Riverpod es la eleccion correcta

Pasa a AsyncValue cuando el resultado asincrono deja de ser un detalle privado de un widget.

El mismo render de tres estados, ahora con origen en un provider:

// Flutter 3.44, Dart 3.12, flutter_riverpod 3.3.1
final userProvider = FutureProvider.family<User, String>((ref, id) {
  return api.fetchUser(id); // runs once, cached per id, shared everywhere
});

class UserCard extends ConsumerWidget {
  const UserCard({super.key, required this.id});
  final String id;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider(id));
    return switch (user) {
      AsyncData(:final value) => Text(value.name),
      AsyncError(:final error) => Text('Failed: $error'),
      _ => const CircularProgressIndicator(),
    };
  }
}

Dos widgets que llaman a ref.watch(userProvider('42')) comparten un fetch y un resultado cacheado. No hay initState, ni campo almacenado, ni disciplina de “crear el Future una vez” que recordar, porque el provider ya ejecuta su build exactamente una vez por argumento hasta que se invalida. Para el conjunto completo de estados, mutaciones con AsyncValue.guard y mantener los datos previos al refrescar, consulta como mostrar estados de carga y error con AsyncValue.

El comportamiento de reconstruccion y refetch que en realidad decide

El rendimiento no es el eje aqui. Ambos enfoques renderizan a la misma tasa de frames. Lo que difiere es cuantas veces se ejecuta tu trabajo asincrono, y eso es un problema de correccion y de costo, no de velocidad bruta.

Pon un contador dentro de la llamada asincrona y observa que pasa cuando el widget circundante se reconstruye (un cambio de tema, un teclado que se abre, un setState del padre):

Si tu trabajo asincrono es una lectura local barata, nada de esto importa y el builder gana en simplicidad. Si es una llamada de red, una consulta a base de datos o cualquier cosa con un costo o un limite de tasa, el caching es la razon completa por la que existe AsyncValue, y reimplementar el mismo comportamiento a mano alrededor de FutureBuilder reimplementa una version peor de la cache de provider de Riverpod.

El punto delicado que decide por ti

Algunas restricciones zanjan la decision sin importar el gusto.

Ya estas usando Riverpod. Si la app tiene providers, no mezcles FutureBuilder en una pantalla que los lee. Leer los datos de un provider y luego envolver un segundo FutureBuilder alrededor de otra llamada asincrona te da dos ciclos de vida no relacionados en una pantalla y dos lugares donde “loading” puede ser true. Expon la segunda llamada como un provider tambien y renderiza ambos con AsyncValue. La consistencia aqui previene la clase de bug donde una mitad de la pantalla queda desactualizada.

El resultado debe sobrevivir al widget. Cualquier cosa traida en initState muere con el State. Si el usuario navega hacia adelante y vuelve y no quieres un spinner nuevo y una llamada de red nueva cada vez, necesitas una cache que viva por encima del widget. Eso es un provider. FutureBuilder no puede darte persistencia entre rutas sin importar como lo organices.

Tocas ref despues de un await. Esta es una trampa especifica de Riverpod, no una razon para evitarlo: si haces await dentro de un notifier y luego lees ref despues de que el widget que lo disparo ya no existe, te encuentras con Cannot use "ref" after the widget was disposed. La solucion es capturar lo que necesitas antes del await. Vale la pena saberlo antes de comprometerte, y se cubre en la solucion para usar ref despues de la disposicion.

Quieres explicitamente cero dependencias. Un ejemplo de paquete pub, un caso de reproduccion o una politica de equipo contra las bibliotecas de gestion de estado fuerza los builders. Esa es una restriccion legitima, y los builders son perfectamente capaces para UI asincrona autocontenida.

StreamBuilder tiene un matiz extra

Todo lo anterior aplica al trabajo con Future. Los streams agregan un ciclo de vida de suscripcion, y eso inclina la decision un poco mas hacia Riverpod para cualquier cosa no trivial. StreamBuilder se resuscribe cuando le pasas una nueva instancia de Stream y se desuscribe cuando sale del arbol, pero no hace multicast: dos StreamBuilder sobre el mismo stream de suscripcion unica lanzaran un error, porque un Stream de suscripcion unica permite solo un listener. El StreamProvider de Riverpod se sienta delante del stream, asi que multiples widgets leen un AsyncValue sin pelear por la suscripcion, y el ultimo valor se cachea para los suscriptores tardios. Si un stream se muestra en exactamente un lugar, StreamBuilder esta bien. Si mas de un widget lo necesita, StreamProvider elimina el problema del listener unico por completo.

La recomendacion, con todo el contexto detras

Por defecto, usa AsyncValue de Riverpod para cualquier resultado asincrono que se comparta, se cachee, se refresque o se mute, que en una app real es la mayoria de ellos. Obtienes un fetch en lugar de N, caching gratis entre navegaciones, un isLoading que preserva los datos previos al refrescar y logica que puedes probar sin un widget. Conserva FutureBuilder y StreamBuilder para UI asincrona genuinamente autocontenida y desechable: un widget hoja que carga una cosa, la muestra y la olvida al desmontarse, especialmente en apps que no cargan ninguna dependencia de gestion de estado. Los builders no son rueditas de entrenamiento que superas. Son la herramienta correcta cuando el resultado asincrono tiene una audiencia de uno, y la herramienta incorrecta en el momento en que tiene una audiencia de dos. Elige por propiedad, no por familiaridad.

Si todavia estas eligiendo un enfoque de gestion de estado de forma mas amplia, las compensaciones entre paquetes estan en Provider vs Riverpod vs Bloc para gestion de estado en Flutter en 2026. Y si tu UI asincrona sigue mostrando fallos, como manejar errores de red con elegancia en una app Flutter cubre como convertir excepciones lanzadas en un estado de error limpio en ambos modelos.

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver