Start Debugging

FutureBuilder/StreamBuilder vs AsyncValue von Riverpod in Flutter: was sollten Sie verwenden?

Verwenden Sie FutureBuilder oder StreamBuilder fuer ein in sich geschlossenes, wegwerfbares asynchrones Widget. Greifen Sie zu AsyncValue von Riverpod, sobald das Ergebnis geteilt, gecacht oder mutiert wird. Hier sind die Entscheidung, die Fallstricke und ausfuehrbarer Code fuer beide. Getestet mit Flutter 3.44 und flutter_riverpod 3.3.1.

Wenn Sie sich zwischen den eingebauten FutureBuilder / StreamBuilder von Flutter und dem AsyncValue von Riverpod entscheiden, lautet die kurze Antwort: Behalten Sie die Builder fuer ein einzelnes, in sich geschlossenes Widget, das ein wegwerfbares asynchrones Ergebnis besitzt, und wechseln Sie zu AsyncValue von Riverpod in dem Moment, in dem dieses Ergebnis ueber Bildschirme hinweg geteilt, gecacht, aktualisiert oder mutiert wird. Die Builder sind nicht “die Anfaengerversion” derselben Sache. Sie sind eine UI-Primitive, die ein asynchrones Objekt abonniert. AsyncValue ist ein Zustandsmodell, das ausserhalb des Widget-Baums lebt. Diese Anleitung wurde mit Flutter 3.44 (stabil, 2026-05-18), Dart 3.12 und flutter_riverpod 3.3.1 getestet (die 3.0-Linie erschien am 2025-09-10).

Sie loesen ueberlappende Probleme auf unterschiedlichen Ebenen

FutureBuilder und StreamBuilder sind Widgets. Sie uebergeben jedem ein Future oder Stream, und es gibt Ihrem builder-Callback ein AsyncSnapshot<T>, das den aktuellen Verbindungszustand (waiting, active, done) plus die neuesten Daten oder den Fehler beschreibt. Das Widget abonniert beim Einfuegen, kuendigt beim Entfernen und abonniert erneut, wenn Sie eine andere Future/Stream-Instanz uebergeben. Das ist der gesamte Vertrag. Es gibt kein Caching, kein Teilen und kein Gedaechtnis fuer das Ergebnis, sobald das Widget den Baum verlaesst.

Das AsyncValue<T> von Riverpod ist ueberhaupt kein Widget. Es ist eine versiegelte Union mit drei Untertypen (AsyncData, AsyncLoading, AsyncError), die ein Provider als seinen Wert bereitstellt. Die asynchrone Arbeit laeuft innerhalb eines Providers, der ausserhalb des Widget-Baums lebt, sodass jedes Widget sie lesen kann, mehrere Widgets dieselbe Instanz lesen koennen und das Ergebnis Neuaufbauten und die Navigation ueberlebt. Sie rendern es mit value.when(...) oder einem Dart-3-switch, genauso wie Sie ein AsyncSnapshot rendern, aber die Quelle der Wahrheit ist ein Provider statt eines Widget-Felds.

Die eigentliche Frage ist also nicht “was rendert drei Zustaende besser”. Beide rendern drei Zustaende gut. Die Frage ist, wo das asynchrone Ergebnis leben soll und wie viele Dinge es sehen muessen.

Funktionsmatrix

AspektFutureBuilder / StreamBuilder (Flutter 3.44)AsyncValue von Riverpod (flutter_riverpod 3.3.1)
Was es istEin Widget, das ein Future/Stream abonniertEin versiegelter Zustandstyp, den ein Provider bereitstellt
Wo das Ergebnis lebtIm Widget, stirbt beim Aushaengen des WidgetsIn einem Provider, ausserhalb des Baums, ueberlebt die Navigation
Teilen ueber BildschirmeNein, jeder Builder fuehrt seine eigene Arbeit erneut ausJa, ein Provider von vielen Widgets gelesen
Caching / DedupKeines, Sie memoisieren das Future selbstEingebaut, der Provider cacht bis zur Invalidierung
Ausloesung bei jedem NeuaufbauJa, wenn das Future in build erstellt wirdNein, der build des Providers laeuft einmal bis zur Invalidierung
Loading + vorherige DatenManuell, der Snapshot verliert data waehrend des Wartensvalue.isLoading behaelt value waehrend der Aktualisierung
Mutationen / AktualisierungFuture neu zuweisen und setStateref.invalidate oder AsyncValue.guard in einem Notifier
Testen ohne ein WidgetSchwierig, braucht pumpWidgetEinfach, lesen Sie den Provider in einem einfachen ProviderContainer
AbhaengigkeitenNull, kommt mit dem SDKPaket flutter_riverpod
Boilerplate-Zeilen fuer EinmaligesMinimalMehr Einrichtung fuer einen einzelnen wegwerfbaren Aufruf

Wann FutureBuilder oder StreamBuilder die richtige Wahl ist

Greifen Sie zu den eingebauten Buildern, wenn das asynchrone Ergebnis wirklich zu einem Widget gehoert und niemand sonst es braucht.

Hier ist die korrekte Form. Das Future wird einmal in initState erstellt, nicht in build, sodass das Widget nicht bei jedem Neuaufbau des Eltern-Widgets erneut abruft.

// 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(),
        };
      },
    );
  }
}

Der haeufigste Bug bei diesem Widget ist, das Future inline zu erstellen, wie future: api.fetchUser(widget.id) direkt in build. Jeder Neuaufbau allokiert dann ein neues Future, FutureBuilder sieht eine neue Identitaet und startet erneut aus dem Loading-Zustand. Dieser Fehlermodus ist haeufig genug, um einen eigenen Beitrag zu haben: siehe warum FutureBuilder sein Future bei jedem Neuaufbau neu erstellt fuer die vollstaendige Reproduktion und jede Variante, die ihn ausloest.

Wann AsyncValue von Riverpod die richtige Wahl ist

Wechseln Sie zu AsyncValue, wenn das asynchrone Ergebnis aufhoert, ein privates Detail eines Widgets zu sein.

Dasselbe Rendern dreier Zustaende, jetzt aus einem Provider bezogen:

// 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(),
    };
  }
}

Zwei Widgets, die ref.watch(userProvider('42')) aufrufen, teilen einen Abruf und ein gecachtes Ergebnis. Es gibt kein initState, kein gespeichertes Feld und keine Disziplin “das Future einmal erstellen”, an die man sich erinnern muss, weil der Provider seinen build bereits genau einmal pro Argument ausfuehrt, bis er invalidiert wird. Fuer die vollstaendige Menge an Zustaenden, Mutationen mit AsyncValue.guard und das Behalten vorheriger Daten bei der Aktualisierung siehe wie man Loading- und Fehlerzustaende mit AsyncValue anzeigt.

Das Verhalten bei Neuaufbau und erneutem Abruf, das tatsaechlich entscheidet

Performance ist hier nicht die Achse. Beide Ansaetze rendern mit derselben Bildrate. Was sich unterscheidet, ist, wie oft Ihre asynchrone Arbeit laeuft, und das ist eine Frage der Korrektheit und der Kosten, nicht der reinen Geschwindigkeit.

Setzen Sie einen Zaehler in den asynchronen Aufruf und beobachten Sie, was passiert, wenn das umgebende Widget neu aufgebaut wird (ein Themenwechsel, eine sich oeffnende Tastatur, ein setState des Eltern-Widgets):

Wenn Ihre asynchrone Arbeit ein billiges lokales Lesen ist, spielt nichts davon eine Rolle und der Builder gewinnt durch Einfachheit. Wenn es ein Netzwerkaufruf, eine Datenbankabfrage oder irgendetwas mit Kosten oder einem Ratenlimit ist, ist das Caching der ganze Grund, warum AsyncValue existiert, und dasselbe Verhalten von Hand um FutureBuilder herum nachzubauen reimplementiert eine schlechtere Version des Provider-Caches von Riverpod.

Der Fallstrick, der fuer Sie entscheidet

Einige Einschraenkungen klaeren die Entscheidung unabhaengig vom Geschmack.

Sie verwenden bereits Riverpod. Wenn die App Provider hat, mischen Sie keinen FutureBuilder in einen Bildschirm, der sie liest. Die Daten eines Providers zu lesen und dann einen zweiten FutureBuilder um einen anderen asynchronen Aufruf zu wickeln, gibt Ihnen zwei unzusammenhaengende Lebenszyklen auf einem Bildschirm und zwei Stellen, an denen “loading” true sein kann. Stellen Sie den zweiten Aufruf ebenfalls als Provider bereit und rendern Sie beide mit AsyncValue. Konsistenz hier verhindert die Klasse von Bugs, bei denen eine Haelfte des Bildschirms veraltet ist.

Das Ergebnis muss das Widget ueberleben. Alles, was in initState abgerufen wird, stirbt mit dem State. Wenn der Benutzer vorwaerts und zurueck navigiert und Sie nicht jedes Mal einen frischen Spinner und einen frischen Netzwerkaufruf wollen, brauchen Sie einen Cache, der ueber dem Widget lebt. Das ist ein Provider. FutureBuilder kann Ihnen keine routenuebergreifende Persistenz geben, egal wie Sie es arrangieren.

Sie greifen nach einem await auf ref zu. Das ist eine Riverpod-spezifische Falle, kein Grund, es zu vermeiden: Wenn Sie innerhalb eines Notifiers await machen und dann ref lesen, nachdem das Widget, das ihn ausgeloest hat, weg ist, treffen Sie auf Cannot use "ref" after the widget was disposed. Die Loesung besteht darin, das, was Sie brauchen, vor dem await zu erfassen. Es lohnt sich, das zu wissen, bevor Sie sich festlegen, und es wird in der Loesung fuer die Verwendung von ref nach dem Verwerfen behandelt.

Sie wollen ausdruecklich null Abhaengigkeiten. Ein Pub-Paketbeispiel, ein Reproduktionsfall oder eine Teamrichtlinie gegen Bibliotheken zur Zustandsverwaltung erzwingt die Builder. Das ist eine legitime Einschraenkung, und die Builder sind fuer in sich geschlossene asynchrone UI vollkommen geeignet.

StreamBuilder hat eine zusaetzliche Feinheit

Alles oben Genannte gilt fuer Future-Arbeit. Streams fuegen einen Abonnement-Lebenszyklus hinzu, und das neigt die Entscheidung fuer alles Nicht-Triviale ein wenig weiter zu Riverpod. StreamBuilder abonniert erneut, wenn Sie ihm eine neue Stream-Instanz uebergeben, und meldet sich ab, wenn er den Baum verlaesst, aber er macht kein Multicast: Zwei StreamBuilder auf demselben Single-Subscription-Stream werfen einen Fehler, weil ein Single-Subscription-Stream nur einen Listener erlaubt. Der StreamProvider von Riverpod sitzt vor dem Stream, sodass mehrere Widgets ein AsyncValue lesen, ohne um das Abonnement zu kaempfen, und der neueste Wert wird fuer spaete Abonnenten gecacht. Wenn ein Stream an genau einer Stelle angezeigt wird, ist StreamBuilder in Ordnung. Wenn mehr als ein Widget ihn braucht, beseitigt StreamProvider das Single-Listener-Problem vollstaendig.

Die Empfehlung, mit dem vollen Kontext dahinter

Verwenden Sie standardmaessig AsyncValue von Riverpod fuer jedes asynchrone Ergebnis, das geteilt, gecacht, aktualisiert oder mutiert wird, was in einer echten App die meisten sind. Sie erhalten einen Abruf statt N, kostenloses Caching ueber Navigationen hinweg, ein isLoading, das vorherige Daten bei der Aktualisierung bewahrt, und Logik, die Sie ohne ein Widget testen koennen. Behalten Sie FutureBuilder und StreamBuilder fuer wirklich in sich geschlossene, wegwerfbare asynchrone UI: ein Blatt-Widget, das eine Sache laedt, sie anzeigt und beim Aushaengen vergisst, besonders in Apps, die keine Abhaengigkeit zur Zustandsverwaltung tragen. Die Builder sind keine Stuetzraeder, die man entwaechst. Sie sind das richtige Werkzeug, wenn das asynchrone Ergebnis ein Publikum von einem hat, und das falsche Werkzeug in dem Moment, in dem es ein Publikum von zwei hat. Waehlen Sie nach Eigentuemerschaft, nicht nach Vertrautheit.

Wenn Sie noch breiter einen Ansatz zur Zustandsverwaltung waehlen, finden sich die Abwaegungen zwischen den Paketen in Provider vs Riverpod vs Bloc fuer Flutter-Zustandsverwaltung in 2026. Und wenn Ihre asynchrone UI weiterhin Fehler zutage foerdert, behandelt wie man Netzwerkfehler in einer Flutter-App elegant behandelt, wie man geworfene Ausnahmen in beiden Modellen in einen sauberen Fehlerzustand umwandelt.

Quellen

Comments

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

< Zurück