Caching-Strategien in Flutter: was lokal speichern, wann invalidieren und wie man Bildschirme konsistent hält — Regeln für Frische, TTLs und Invalidation.

Caching in einer mobilen App bedeutet, eine Kopie von Daten in der Nähe zu halten (im Speicher oder auf dem Gerät), damit der nächste Bildschirm sofort rendern kann, statt auf das Netzwerk zu warten. Diese Daten können eine Liste von Items, ein Benutzerprofil oder Suchergebnisse sein.
Das Problem ist, dass gecachte Daten oft leicht falsch sind. Nutzer merken das schnell: ein Preis, der nicht aktualisiert wird, eine Badge-Anzeige, die sich festgefahren anfühlt, oder ein Detailbildschirm, der kurz nach einer Änderung alte Informationen zeigt. Was das Debuggen schwierig macht, ist das Timing. Derselbe Endpunkt kann nach Pull-to-Refresh in Ordnung aussehen, aber nach Zurück-Navigieren, App-Resume oder Account-Wechsel falsch.
Es gibt einen echten Kompromiss. Wenn du immer frische Daten holst, wirken Bildschirme langsam und springen, und du verschwendest Akku und Daten. Wenn du aggressiv cacht, fühlt sich die App schnell an, aber Nutzer hören auf, dem Gesehenen zu vertrauen.
Ein einfaches Ziel hilft: mache Frische vorhersehbar. Entscheide, was jeder Bildschirm zeigen darf (frisch, leicht veraltet oder offline), wie lange Daten leben dürfen, bevor du sie erneuerst, und welche Ereignisse sie ungültig machen müssen.
Stell dir einen typischen Ablauf vor: ein Nutzer öffnet eine Bestellung und geht dann zurück zur Bestellliste. Kommt die Liste aus dem Cache, zeigt sie möglicherweise noch den alten Status. Wenn du bei jedem Aufruf neu lädst, kann die Liste flackern und langsam wirken. Klare Regeln wie „zeige Cache sofort, aktualisiere im Hintergrund und aktualisiere beide Bildschirme, wenn die Antwort eintrifft“ machen das Erlebnis über Navigation hinweg konsistent.
Ein Cache ist nicht nur "gespeicherte Daten". Es ist eine gespeicherte Kopie plus eine Regel dafür, wann diese Kopie noch gültig ist. Wenn du nur das Payload speicherst, aber die Regel weglässt, endest du mit zwei Realitäten: ein Bildschirm zeigt neue Infos, ein anderer die von gestern.
Ein praktisches Modell ist, jedes gecachte Element in einen von drei Zuständen zu setzen:
Diese Einordnung macht die UI vorhersehbar, weil sie jedes Mal gleich reagieren kann, wenn sie einen Zustand sieht.
Frische-Regeln sollten auf erklärbaren Signalen basieren. Übliche Optionen sind zeitbasierte Abläufe (z. B. 5 Minuten), Versionsänderungen (Schema oder App-Version), Nutzeraktionen (Pull-to-Refresh, Submit, Delete) oder Server-Hinweise (ETag, last-updated-Timestamp oder eine explizite „cache invalid“-Antwort).
Beispiel: Ein Profilbildschirm lädt gecachte Nutzerdaten sofort. Ist es "stale-but-usable", zeigt er den gecachten Namen und Avatar und aktualisiert still im Hintergrund. Hat der Nutzer gerade sein Profil bearbeitet, ist das ein must-refresh-Moment. Die App sollte den Cache sofort aktualisieren, damit alle Bildschirme konsistent bleiben.
Entscheide, wer diese Regeln besitzt. Meistens ist die beste Default-Regel: die Datenebene (data layer) besitzt Frische und Invalidierung, die UI reagiert nur (zeige Cache, zeige Ladezustand, zeige Fehler) und das Backend gibt Hinweise, wenn möglich. Das verhindert, dass jeder Bildschirm seine eigenen Regeln erfindet.
Gutes Caching beginnt mit einer Frage: Schadet es dem Nutzer, wenn diese Daten etwas veraltet sind? Ist die Antwort „wahrscheinlich nicht“, passt es meist für lokalen Cache.
Daten, die oft gelesen werden und sich langsam ändern, lohnen sich zu cachen: Feeds und Listen, über die Nutzer viel scrollen, Kataloginhalte (Produkte, Artikel, Templates) und Referenzdaten wie Kategorien oder Länder. Einstellungen und Präferenzen gehören ebenfalls dazu, ebenso grundlegende Profilinfos wie Name und Avatar-URL.
Risikohaft sind alles Geld- oder zeitkritische Daten. Kontostände, Zahlungsstatus, Verfügbarkeiten, Terminfenster, Liefer-ETAs und „zuletzt online“-Angaben können echte Probleme verursachen, wenn sie veraltet sind. Du kannst sie zur Beschleunigung cachen, musst aber den Cache vor Entscheidungszeitpunkten invalidieren (z. B. direkt vor Abschluss einer Bestellung).
Abgeleiteter UI-Zustand ist eine eigene Kategorie. Das Speichern des ausgewählten Tabs, Filter, Suchanfrage, Sortierung oder der Scroll-Position kann die Navigation glatt erscheinen lassen. Es irritiert aber auch, wenn alte Entscheidungen unerwartet wieder auftauchen. Eine einfache Regel: halte UI-Zustand im Speicher, solange der Nutzer in diesem Flow bleibt, und setze ihn zurück, wenn er bewusst "neu startet" (z. B. Rückkehr zum Home-Bildschirm).
Vermeide das Cachen von Dingen, die Sicherheits- oder Datenschutzrisiken schaffen: Geheimnisse (Passwörter, API-Keys), Einmal-Tokens (OTP, Passwort-Reset-Tokens) und sensible persönliche Daten, außer du brauchst wirklich Offline-Zugriff. Speichere niemals vollständige Kartendetails oder etwas, das das Betrugsrisiko erhöht.
In einer Shopping-App ist das Cachen der Produktliste ein großer Gewinn. Der Checkout-Bildschirm sollte jedoch immer Totals und Verfügbarkeit direkt vor dem Kauf bestätigen.
Die meisten Flutter-Apps brauchen einen lokalen Cache, damit Bildschirme schnell laden und nicht leer aufwachen, bis das Netzwerk antwortet. Die zentrale Entscheidung ist, wo gecachte Daten leben, denn jede Ebene hat unterschiedliche Geschwindigkeit, Größenlimits und Aufräumverhalten.
Ein Memory-Cache ist am schnellsten. Er ist ideal für Daten, die du gerade abgerufen hast und innerhalb der geöffneten App erneut verwenden wirst, wie das aktuelle Profil, die letzten Suchergebnisse oder ein gerade angesehenes Produkt. Der Nachteil: Er verschwindet, wenn die App beendet wird, hilft also nicht bei Cold-Starts oder Offline-Nutzung.
Disk Key-Value-Speicher eignet sich für kleine Items, die zwischen Neustarts erhalten bleiben sollen: Einstellungen, "zuletzt ausgewählter Tab" und kleine JSON-Antworten, die sich selten ändern. Halte es bewusst klein — große Listen in Key-Value zu lagern macht Updates kompliziert und lässt den Cache schnell aufblähen.
Eine lokale Datenbank ist die richtige Wahl, wenn Daten größer, strukturiert oder offline-fähig sein müssen. Sie hilft auch, wenn du Abfragen brauchst ("alle ungelesenen Nachrichten", "Warenkorb-Items", "Bestellungen des letzten Monats") statt eine große Blob-Datei zu laden und in Memory zu filtern.
Um Caching vorhersehbar zu halten, wähle für jeden Datentyp eine primäre Speicherstätte und vermeide, dass derselbe Datensatz an drei Orten liegt.
Kurzregeln:
Plane auch Größe und Aufräumen. Definiere, was „zu groß“ ist, wie lange du Einträge hältst und wie du bereinigst. Beispiel: Beschränke gecachte Suchergebnisse auf die letzten 20 Queries und entferne regelmäßig Einträge älter als 30 Tage, damit der Cache nicht unbemerkt wächst.
Refresh-Regeln sollten so einfach sein, dass du sie einem Kollegen in einem Satz erklären kannst. Dann zahlt sich vernünftiges Caching aus: Nutzer erleben schnelle Bildschirme, und die App bleibt vertrauenswürdig.
Die einfachste Regel ist TTL (time to live). Speicher Daten mit einem Zeitstempel und behandle sie für z. B. 5 Minuten als frisch. Danach sind sie veraltet. TTL passt gut zu "geschenkten" Daten wie Feeds, Kategorien oder Empfehlungen.
Eine nützliche Verfeinerung ist die Trennung von soft TTL und hard TTL.
Bei soft TTL zeigst du gecachte Daten sofort, dann aktualisierst du im Hintergrund und aktualisierst die UI, falls sich etwas geändert hat. Bei hard TTL hörst du auf, alte Daten nach Ablauf zu zeigen: du blockierst mit einem Loader oder zeigst einen "offline/try again"-Zustand. Hard TTL passt, wenn falsch zu liegen schlimmer ist als langsam zu sein (Kontostände, Bestellstatus, Berechtigungen).
Wenn dein Backend es unterstützt, bevorzuge "nur aktualisieren, wenn sich etwas geändert" mit ETag, updatedAt oder einem Versionsfeld. Die App kann fragen "hat sich etwas verändert?" und das vollständige Payload überspringen, wenn nichts neu ist.
Ein benutzerfreundlicher Default für viele Bildschirme ist stale-while-revalidate: sofort zeigen, still aktualisieren und nur neu zeichnen, wenn das Ergebnis abweicht. Das gibt Geschwindigkeit ohne zufälligen Flicker.
Typische Per-Screen-Frische-Regeln könnten so aussehen:
Wähle Regeln basierend auf den Kosten, falsch zu liegen — nicht nur den Fetch-Kosten.
Invalidierung beginnt mit einer Frage: Welches Ereignis macht gecachte Daten weniger vertrauenswürdig als die Kosten des Neuladens? Wenn du eine kleine Menge von Triggern wählst und dich daran hältst, bleibt das Verhalten vorhersehbar und die UI fühlt sich stabil an.
Wichtige Trigger in echten Apps:
Beispiel: Ein Nutzer ändert sein Profilfoto und geht zurück. Wenn du nur zeitbasiert aktualisierst, zeigt der vorherige Bildschirm möglicherweise weiter das alte Bild. Stattdessen behandel die Bearbeitung als Trigger: aktualisiere das gecachte Profilobjekt sofort und markiere es mit neuem Timestamp als frisch.
Halte Invalidierungsregeln klein und explizit. Wenn du das genaue Ereignis, das einen Cache-Eintrag invalidiert, nicht benennen kannst, wirst du entweder zu oft (langsam, springend) oder zu selten (veraltete Bildschirme) aktualisieren.
Beginne damit, deine wichtigsten Bildschirme aufzulisten und welche Daten jeder braucht. Denk nicht in Endpunkten, sondern in nutzersichtbaren Objekten: Profil, Warenkorb, Bestellliste, Katalogeintrag, ungelesene Anzahl.
Wähle dann eine einzige Quelle der Wahrheit pro Datentyp. In Flutter ist das meist ein Repository, das verbirgt, woher die Daten kommen (Memory, Disk, Netzwerk). Bildschirme sollten nicht entscheiden, wann das Netzwerk aufgerufen wird. Sie fragen das Repository und reagieren auf den zurückgegebenen Zustand.
Ein praktischer Flow:
Metadaten machen Regeln durchsetzbar. Wenn ownerUserId sich ändert (Logout/Login), kannst du alte gecachte Reihen sofort verwerfen oder ignorieren, statt kurz Daten des vorherigen Nutzers anzuzeigen.
Für das UI-Verhalten entscheide vorher, was "stale" bedeutet. Eine gängige Regel: zeige stale-Daten sofort, damit der Bildschirm nicht leer ist, starte einen Hintergrund-Refresh und aktualisiere bei neuen Daten. Scheitert der Refresh, behalte die stale-Daten und zeige einen kleinen, klaren Fehler.
Dann sperre die Regeln mit ein paar langweiligen Tests:
Das ist der Unterschied zwischen „wir haben Caching“ und „unsere App verhält sich jedes Mal gleich".
Nichts zerstört Vertrauen schneller, als einen Wert in der Liste zu sehen, ins Detail zu tippen, ihn zu bearbeiten und beim Zurückkehren wieder den alten Wert zu sehen. Konsistenz kommt daher, dass jeder Bildschirm aus derselben Quelle liest.
Eine solide Regel lautet: einmal fetchen, einmal speichern, vielfach rendern. Bildschirme sollten nicht denselben Endpunkt unabhängig aufrufen und private Kopien behalten. Leg die gecachten Daten in einen gemeinsamen Store (dein State-Management) und lass sowohl Listen- als auch Detailansichten denselben Datenstand beobachten.
Behalte einen zentralen Ort, der den aktuellen Wert und dessen Frische besitzt. Bildschirme können einen Refresh anstoßen, sollten aber nicht ihre eigenen Timer, Retries und Parser managen.
Praktische Gewohnheiten, die „zwei Realitäten“ verhindern:
Auch mit guten Regeln sehen Nutzer manchmal stale Daten (offline, langsames Netz, backgrounded App). Mach das sichtbar mit kleinen, ruhigen Signalen: einem "Vor kurzem aktualisiert"-Zeitstempel, einem dezenten "Aktualisiere…"-Indikator oder einem "Offline"-Badge.
Bei Bearbeitungen fühlen sich optimistische Updates meist am besten an. Beispiel: Der Nutzer ändert im Detailbildschirm den Produktpreis. Aktualisiere den gemeinsamen Store sofort, damit die Liste beim Zurückkehren den neuen Preis zeigt. Schlägt das Speichern fehl, rolle zurück und zeige eine kurze Fehlermeldung.
Die meisten Caching-Ausfälle sind langweilig: Der Cache funktioniert, aber niemand kann erklären, wann er verwendet wird, wann er abläuft und wer ihn besitzt.
Die erste Falle ist Cachen ohne Metadaten. Wenn du nur das Payload speicherst, kannst du nicht sagen, ob es alt ist, welche App-Version es erzeugte oder welchem Nutzer es gehört. Speichere mindestens savedAt, eine einfache Versionsnummer und eine userId. Diese kleine Gewohnheit verhindert viele "Warum zeigt dieser Bildschirm falsche Daten?"-Bugs.
Ein weiteres Problem sind mehrere Caches für dieselben Daten ohne Besitzer. Eine Liste hält eine In-Memory-Liste, ein Repository schreibt auf die Disk und ein Detailbildschirm fetcht neu und speichert anderswo. Wähle eine Quelle der Wahrheit (oft die Repository-Ebene) und lasse jeden Bildschirm darüber lesen.
Account-Wechsel sind ein häufiger Stolperstein. Wenn sich jemand ausloggt oder das Konto wechselt, lösche nutzerbezogene Tabellen und Keys. Sonst siehst du kurz das Profilfoto oder die Bestellungen des vorherigen Nutzers, was wie ein Datenschutzproblem wirkt.
Praktische Fixes:
Beispiel: Deine Produktliste lädt sofort aus dem Cache und aktualisiert leise. Scheitert der Refresh, zeige weiter gecachte Daten, mache aber klar, dass sie veraltet sein könnten, und biete Retry an. Blockiere die UI nicht, wenn gepufferte Daten völlig in Ordnung sind.
Bevor du veröffentlichst, verwandle Caching von "scheint in Ordnung" in Regeln, die du testen kannst. Nutzer sollten sinnvolle Daten sehen, selbst nach Hin- und Her-Navigation, Offline-Nutzung oder Login mit einem anderen Konto.
Für jeden Bildschirm entscheide, wie lange Daten frisch bleiben können. Das können Minuten für schnell bewegliche Daten (Nachrichten, Kontostände) oder Stunden für langsam ändernde Daten (Einstellungen, Produktkategorien) sein. Bestimme dann, was passiert, wenn Daten nicht mehr frisch sind: Hintergrund-Refresh, Refresh beim Öffnen oder manuelles Pull-to-Refresh.
Für jeden Datentyp entscheide, welche Ereignisse Cache leeren oder umgehen müssen. Gängige Trigger: Logout, Edit des Items, Account-Wechsel und App-Updates, die die Datenform ändern.
Stelle sicher, dass gecachte Einträge kleine Metadaten neben dem Payload speichern:
Halte die Ownership klar: nutze ein Repository pro Datentyp (z. B. ProductsRepository), nicht pro Widget. Widgets fordern Daten an, sie entscheiden nicht über Cache-Regeln.
Definiere und teste offline-Verhalten: Was zeigen Bildschirme aus dem Cache, welche Aktionen sind deaktiviert, welcher Text erscheint ("Gespeicherte Daten anzeigen" plus sichtbare Refresh-Kontrolle). Manuelles Refresh sollte auf jedem cache-gestützten Bildschirm vorhanden und leicht zu finden sein.
Stell dir eine einfache Shop-App mit drei Bildschirmen vor: Produktkatalog (Liste), Produktdetails und ein Favoriten-Tab. Nutzer scrollen, öffnen ein Produkt und tippen auf das Herz, um es zu favorisieren. Ziel ist, schnell zu wirken, auch bei schlechten Netzen, ohne verwirrende Inkonsistenzen.
Cachiere lokal, was dir hilft, sofort zu rendern: Katalogseiten (IDs, Titel, Preis, Thumbnail-URL, Favoriten-Flag), Produktdetails (Beschreibung, Specs, Verfügbarkeit, lastUpdated), Bildmetadaten (URLs, Größen, Cache-Keys) und die Favoriten des Nutzers (Set von Produkt-IDs, optional mit Timestamps).
Beim Öffnen des Katalogs zeige gecachte Ergebnisse sofort und revalidiere im Hintergrund. Kommen frische Daten, aktualisiere nur, was sich geändert hat, und bewahre die Scroll-Position.
Für den Favoriten-Toggle behandle ihn als "muss konsistent sein"-Aktion. Aktualisiere die lokale Favoritenmenge sofort (optimistisches Update) und aktualisiere gecachte Produktzeilen und Produktdetails für diese ID. Scheitert der Netzwerk-Call, rolle zurück und zeige eine kurze Nachricht.
Um Navigation konsistent zu halten, treibe Badges in der Liste und das Herz-Icon im Detail von derselben Quelle der Wahrheit (dein lokaler Cache/Store), nicht von getrenntem Screen-State. Die Liste aktualisiert sich sofort beim Zurückkehren, das Detail reflektiert Änderungen aus der Liste, und der Favoriten-Zähler stimmt überall, ohne auf einen Refetch zu warten.
Füge einfache Refresh-Regeln hinzu: Katalog-Cache läuft schnell ab (Minuten), Produktdetails etwas langsamer, Favoriten laufen nie ab, werden aber nach Login/Logout immer abgeglichen.
Caching hört auf, mysteriös zu sein, wenn dein Team auf einer Seite mit Regeln verweisen kann und sich darauf einigt, was passieren soll. Ziel ist nicht Perfektion, sondern vorhersehbares Verhalten, das sich über Releases hinweg nicht ändert.
Schreibe eine kleine Tabelle pro Bildschirm und halte sie kurz genug, um sie bei Änderungen schnell zu prüfen: Bildschirmname und Hauptdaten, Cache-Ort und Key, Frische-Regel (TTL, ereignisbasiert oder manuell), Invalidierungs-Trigger und was der Nutzer beim Refresh sieht.
Füge leichtgewichtige Logs hinzu, während du feinabstimmst. Protokolliere Cache-Hits, -Misses und warum ein Refresh ausgelöst wurde (TTL abgelaufen, Nutzer hat gezogen, App resümiert, Mutation abgeschlossen). Wenn jemand berichtet: „Diese Liste wirkt falsch", machen solche Logs den Bug lösbar.
Beginne mit einfachen TTLs und verfeinere sie basierend auf Nutzerbeobachtungen. Ein News-Feed kann 5–10 Minuten Stale-Toleranz haben, ein Bestellstatus-Bildschirm sollte beim Resume und nach jedem Checkout eine Aktualisierung verlangen.
Wenn du schnell eine Flutter-App baust, hilft es, die Datenebene und Caching-Regeln vor der Implementierung zu skizzieren. Für Teams, die Koder.ai (koder.ai) nutzen, ist der Planning Mode ein praktischer Ort, um diese pro-Bildschirm Regeln zuerst zu definieren und dann passend umzusetzen.
Beim Abstimmen von Refresh-Verhalten schütze stabile Bildschirme während du experimentierst. Snapshots und Rollbacks sparen Zeit, wenn eine neue Regel versehentlich Flicker, leere Zustände oder inkonsistente Zähler über Navigation einführt.
Beginne mit einer klaren Regel pro Bildschirm: Was darf sofort gezeigt werden (Cache), wann muss neu geladen werden und was sieht der Nutzer während der Aktualisierung. Wenn du die Regel nicht in einem Satz erklären kannst, wird die App irgendwann inkonsistent wirken.
Behandle gecachte Daten als Zustandsmaschine für Frische. Wenn sie fresh sind, zeige sie. Wenn sie stale but usable sind, zeige sie jetzt und aktualisiere still im Hintergrund. Wenn sie must refresh sind, lade neu bevor du zeigst (oder zeige einen Lade-/Offline-Zustand). So verhält sich die UI konsistent statt „manchmal aktualisiert, manchmal nicht“.
Cache Dinge, die oft gelesen werden und bei leichtem Veralten dem Nutzer nicht schaden: Feeds, Kataloge, Referenzdaten und grundlegende Profilinfos. Vorsicht bei geld- oder zeitkritischen Daten wie Kontoständen, Verfügbarkeiten, ETAs oder Bestellstatus: Du kannst sie cachen, musst aber vor einer Entscheidung immer frisch prüfen.
Nutze Memory für schnellen Wiedergebrauch innerhalb der aktuellen Session (aktuelles Profil, zuletzt angeschautes). Nutze Key-Value auf Disk für kleine, einfache Items, die Restarts überdauern sollen (Einstellungen). Nutze eine lokale Datenbank, wenn Daten groß, strukturiert, abfragbar oder offline-brauchbar sein sollen (Nachrichten, Bestellungen, Inventar).
Eine einfache TTL ist ein guter Ausgangspunkt: Daten gelten für eine bestimmte Zeit als frisch, dann wird neu geladen. Für viele Bildschirme ist jedoch „zeige Cache jetzt, aktualisiere im Hintergrund und zeichne nur bei Änderung neu“ besser, weil es leere Bildschirme vermeidet und Flicker reduziert.
Invalidiere bei Ereignissen, die das Vertrauen in den Cache sichtbar senken: Nutzeränderungen (create/update/delete), Login/Logout oder Kontowechsel, App-Resume falls älter als TTL, und explizites Nutzer-Refresh. Halte die Trigger klein und explizit, damit du nicht ständig oder nie aktualisierst.
Lass beide Bildschirme aus derselben Quelle lesen, nicht aus privaten Kopien. Wenn der Nutzer im Detail etwas ändert, aktualisiere das gemeinsame Cache-Objekt sofort, damit die Liste beim Zurückkehren den neuen Wert zeigt; synchronisiere dann mit dem Server und mache bei Fehlern ein Rollback.
Speichere neben dem Payload immer Metadaten, besonders einen Zeitstempel und eine Nutzerkennung. Bei Logout oder Account-Wechsel lösche oder isolier nutzergebundene Cache-Einträge sofort und breche laufende Requests für den alten Nutzer ab, damit nicht kurzzeitig die Daten des Vorbesitzers angezeigt werden.
Standardmäßig: Gecachte Daten weiter anzeigen und einen kleinen, klaren Fehlerzustand mit Retry anbieten, statt den Bildschirm leer zu machen. Wenn der Bildschirm alte Daten nicht sicher zeigen kann, dann verwende eine must-refresh-Regel und zeige ein Lade- oder Offline-Messaging, anstatt veraltete Werte vorzutäuschen.
Lege die Cache-Regeln in der Datenebene (z. B. Repositories) ab, damit alle Bildschirme dasselbe Verhalten teilen. Schreibe zuerst die pro-Bildschirm Frische- und Invalidierungsregeln (z. B. in Koder.ai Planning Mode), und implementiere dann so, dass die UI lediglich auf Zustände reagiert statt eigene Refresh-Logik zu erfinden.