Cachingstrategieën in Flutter: wat lokaal te bewaren, wanneer te verversen en hoe schermen consistent te houden bij navigatie.

Caching in een mobiele app betekent dat je een kopie van data dichtbij bewaart (in geheugen of op het apparaat) zodat het volgende scherm meteen kan renderen in plaats van op het netwerk te wachten. Die data kan een lijst items zijn, een gebruikersprofiel of zoekresultaten.
Het lastige is dat gecachte data vaak net even niet klopt. Gebruikers merken het snel: een prijs die niet update, een badge-aantal dat vast lijkt te staan, of een detailscherm dat oude info toont vlak nadat ze iets hebben veranderd. Wat dit moeilijk te debuggen maakt is timing. Dezelfde endpoint kan er prima uitzien na pull-to-refresh, maar fout na terugnavigatie, app-resume of accountwisseling.
Er is een echte trade-off. Als je altijd verse data haalt, voelen schermen traag en schokkerig en verspil je batterij en data. Als je agressief cachet, voelt de app snel, maar verliezen mensen vertrouwen in wat ze zien.
Een simpel doel helpt: maak versheid voorspelbaar. Bepaal wat elk scherm mag tonen (vers, licht verouderd of offline), hoe lang data mag leven voordat je het ververst, en welke gebeurtenissen het moeten ongeldig maken.
Stel je een veelvoorkomende flow voor: een gebruiker opent een order en gaat terug naar de orderlijst. Als de lijst uit de cache komt, kan die nog de oude status tonen. Als je elke keer ververst, kan de lijst flikkeren en traag aanvoelen. Duidelijke regels zoals “toon cache direct, ververs op de achtergrond en update beide schermen wanneer de respons binnenkomt” maken de ervaring consistent bij navigatie.
Een cache is niet alleen “opgeslagen data.” Het is een opgeslagen kopie plus een regel wanneer die kopie nog geldig is. Als je de payload opslaat maar de regel overslaat, krijg je twee versies van de waarheid: het ene scherm toont nieuwe info, het andere toont die van gisteren.
Een praktisch model is om elk gecachet item in één van drie staten te plaatsen:
Deze indeling houdt je UI voorspelbaar omdat het altijd op dezelfde manier kan reageren op een gegeven staat.
Versheidsregels moeten gebaseerd zijn op signalen die je aan een teamgenoot kunt uitleggen. Gebruikelijke keuzes zijn een tijdsgebaseerde vervaldatum (bijv. 5 minuten), een versieverandering (schema of app-versie), een gebruikersactie (pull-to-refresh, submit, delete) of een serverhint (ETag, last-updated timestamp of een expliciete “cache ongeldig” reactie).
Voorbeeld: een profielscherm laadt direct gecachte gebruikersdata. Als het stale maar bruikbaar is, toont het de gecachte naam en avatar en ververst het stilletjes. Als de gebruiker zojuist zijn profiel heeft bewerkt, is dat een must-refresh moment. De app moet de cache direct bijwerken zodat elk scherm consistent blijft.
Bepaal wie deze regels beheert. In de meeste apps is de beste default: de datalaag beheert versheid en invalidatie, de UI reageert alleen (toon cache, toon laden, toon fout) en de backend geeft hints wanneer dat kan. Dat voorkomt dat elk scherm zijn eigen regels verzint.
Goede caching begint met één vraag: als deze data een beetje oud is, schaadt dat de gebruiker? Als het antwoord “waarschijnlijk niet” is, is het meestal geschikt voor lokale caching.
Data die vaak gelezen wordt en langzaam verandert is typisch de moeite waard om te cachen: feeds en lijsten waar mensen doorheen scrollen, catalogusachtige content (producten, artikelen, templates) en referentiegegevens zoals categorieën of landen. Instellingen en voorkeuren horen hier ook bij, samen met basisprofielinfo zoals naam en avatar-URL.
Het risicovolle deel is alles wat met geld of tijdkritisch te maken heeft. Saldi, betalingsstatus, voorraadbeschikbaarheid, afspraken, bezorgtijden en “laatst online” kunnen echte problemen veroorzaken als ze verouderd zijn. Je kunt ze cachen voor snelheid, maar behandel de cache als tijdelijk en forceer een verversing op beslissende punten (bijv. vlak voordat je een order bevestigt).
Afgeleide UI-state is een eigen categorie. Het opslaan van de geselecteerde tab, filters, zoekopdracht, sortering of scrollpositie kan navigatie soepel doen voelen. Het kan ook verwarring veroorzaken als oude keuzes onverwacht terugkomen. Een eenvoudige regel werkt goed: houd UI-state in geheugen zolang de gebruiker in die flow blijft, maar reset wanneer ze bewust “opnieuw beginnen” (zoals terug naar het homescreen).
Vermijd het cachen van data die een beveiligings- of privacyrisico vormt: geheimen (wachtwoorden, API-sleutels), eenmalige tokens (OTP-codes, wachtwoordresettokens) en gevoelige persoonlijke gegevens tenzij je echt offline toegang nodig hebt. Cache nooit volledige kaartgegevens of iets dat fraude vergemakkelijkt.
In een winkelapp is het cachen van de productlijst een grote winst. Het afreken- of checkoutscherm moet echter altijd totalen en beschikbaarheid verversen vlak voor aankoop.
De meeste Flutter-apps hebben uiteindelijk een lokale cache nodig zodat schermen snel laden en niet leeg flitsen terwijl het netwerk wakker wordt. De sleutelbeslissing is waar gecachte data woont, want elke laag heeft andere snelheid, groottebeperkingen en opruimgedrag.
Een geheugen-cache is het snelst. Het is ideaal voor data die je net hebt opgehaald en hergebruikt tijdens de app-sessie, zoals het huidige gebruikersprofiel, de laatste zoekresultaten of een recent bekeken product. De afweging is eenvoudig: het verdwijnt wanneer de app wordt afgesloten, dus het helpt niet bij koude starts of offline gebruik.
Key-value opslag op schijf past bij kleine items die je over restarts wilt bewaren. Denk aan voorkeuren en simpele blobs: featureflags, “laatst geselecteerde tab” en kleine JSON-responsen die zelden veranderen. Houd het opzettelijk klein. Zodra je grote lijsten in key-value opslag stopt, worden updates lastig en groeit de bloat makkelijk.
Een lokale database is het best als je data groter, gestructureerd is of offline-functionaliteit nodig heeft. Het helpt ook wanneer je queries nodig hebt (“alle ongelezen berichten”, “artikelen in winkelwagen”, “orders van afgelopen maand”) in plaats van één gigantische blob in te laden en in geheugen te filteren.
Om caching voorspelbaar te houden, kies één primaire opslagplaats per type data en vermijd het hetzelfde dataset op drie plekken te houden.
Een snelle vuistregel:
Plan ook voor grootte. Bepaal wat “te groot” betekent, hoe lang je items bewaart en hoe je opruimt. Bijvoorbeeld: beperk gecachte zoekresultaten tot de laatste 20 queries en verwijder regelmatig records ouder dan 30 dagen zodat de cache niet stilletjes eindeloos groeit.
Verversregels moeten simpel genoeg zijn dat je ze in één zin per scherm kunt uitleggen. Daar betaalt caching zich uit: gebruikers krijgen snelle schermen en de app blijft betrouwbaar.
De eenvoudigste regel is TTL (time to live). Sla data op met een timestamp en behandel het als vers voor, zeg, 5 minuten. Daarna wordt het stale. TTL werkt goed voor “nice to have” data zoals een feed, categorieën of aanbevelingen.
Een nuttige verfijning is het splitsen van TTL in soft TTL en hard TTL.
Met een soft TTL toon je gecachte data direct, ververs je op de achtergrond en update je de UI als er iets veranderd is. Met een hard TTL stop je met het tonen van oude data zodra de periode verstreken is. Je blokkeert met een loader of toont een “offline/probeer opnieuw” state. Hard TTL past bij gevallen waar fout zijn erger is dan traag zijn, zoals saldi, orderstatus of permissies.
Als je backend het ondersteunt, heeft de voorkeur: “verversen alleen als het veranderd is” met ETag, updatedAt of een versieveld. Je app kan vragen “is dit veranderd?” en het volledige payload overslaan wanneer niets nieuw is.
Een gebruikersvriendelijke default voor veel schermen is stale-while-revalidate: toon direct, ververs stilletjes en teken opnieuw alleen als het resultaat afwijkt. Het geeft snelheid zonder willekeurige flicker.
Per-scherm versheid ziet er vaak zo uit:
Kies regels op basis van de kosten van fout zitten, niet alleen op basis van fetch-kosten.
Cache-invalidatie begint met één vraag: welk event maakt gecachte data minder betrouwbaar dan de kosten van opnieuw ophalen? Als je een kleine set triggers kiest en je daaraan houdt, blijft het gedrag voorspelbaar en voelt de UI stabiel.
Triggers die in echte apps het meest tellen:
Voorbeeld: een gebruiker wijzigt zijn profielfoto en gaat terug. Als je alleen op tijdsgebaseerde verversing vertrouwt, toont het vorige scherm mogelijk de oude afbeelding tot de volgende fetch. Behandel de edit als trigger: update het gecachte profielobject direct en markeer het als vers met een nieuwe timestamp.
Houd invalidatieregels klein en expliciet. Als je niet exact kunt aanwijzen welk event een cache-entry ongeldig maakt, ververs je te vaak (traag, schokkerige UI) of te weinig (verouderde schermen).
Begin met het opsommen van je sleutel-schermen en de data die elk scherm nodig heeft. Denk niet in endpoints maar in gebruikerszichtbare objecten: profiel, winkelwagen, orderlijst, catalogusitem, ongelezen telling.
Kies vervolgens één bron van waarheid per datatype. In Flutter is dit meestal een repository die verbergt waar data vandaan komt (memory, schijf, netwerk). Schermen zouden niet moeten bepalen wanneer ze het netwerk aanroepen. Ze vragen de repository om data en reageren op de teruggegeven staat.
Een praktisch flow:
Metadata is wat regels afdwingbaar maakt. Als ownerUserId verandert (logout/login), kun je oude gecachte rijen direct verwijderen of negeren in plaats van kort de vorige gebruiker te tonen.
Voor UI-gedrag, bepaal van tevoren wat “stale” betekent. Een veelgebruikte regel: toon stale data meteen zodat het scherm niet leeg is, start een achtergrondverversing en update wanneer nieuwe data arriveert. Als verversen faalt, houd de stale data zichtbaar en toon een klein, duidelijk foutbericht.
Veranker de regels met een paar saaie tests:
Dat is het verschil tussen “we hebben caching” en “onze app gedraagt zich hetzelfde elke keer”.
Niets ondermijnt vertrouwen sneller dan het zien van één waarde in een lijstscherm, doorklikken naar details, het bewerken en bij terugkeer weer de oude waarde zien. Consistentie bij navigatie komt voort uit het laten lezen van elk scherm uit dezelfde bron.
Een stevige regel is: haal één keer op, sla één keer op, render vele keren. Schermen zouden niet onafhankelijk hetzelfde endpoint moeten aanroepen en privékopieën moeten bewaren. Zet gecachte data in een gedeelde store (je state management laag) en laat zowel lijst- als detail-schermen naar dezelfde data kijken.
Houd één plek die de huidige waarde en versheid beheert. Schermen mogen een refresh aanvragen, maar zouden hun eigen timers, retries en parsing niet moeten beheren.
Praktische gewoontes die “twee versies van de waarheid” voorkomen:
Zelfs met goede regels zullen gebruikers soms verouderde data zien (offline, traag netwerk, backgrounded app). Maak dat duidelijk met kleine, rustige signalen: een “Net bijgewerkt” timestamp, een subtiele “Verversen…” indicator of een “Offline” badge.
Voor bewerkingen voelen optimistische updates vaak het beste. Voorbeeld: een gebruiker verandert de prijs van een product op het detail-scherm. Werk de gedeelde store direct bij zodat de lijstscherm bij terugkeer de nieuwe prijs toont. Als opslaan faalt, rol terug naar de vorige waarde en toon een korte foutmelding.
De meeste cachingfouten zijn saai: de cache werkt, maar niemand kan uitleggen wanneer deze gebruikt moet worden, wanneer het vervalt en wie de eigenaar is.
De eerste valkuil is cachen zonder metadata. Als je alleen de payload opslaat, kun je niet zien of het oud is, welke app-versie het produceerde of welke gebruiker het toebehoort. Sla minstens savedAt, een simpele versienummer en een userId (of tenant key) op. Die ene gewoonte voorkomt veel “waarom klopt dit scherm niet?” bugs.
Een ander veelvoorkomend probleem is meerdere caches voor dezelfde data zonder eigenaar. Een lijstscherm houdt een in-memory lijst, een repository schrijft naar schijf en een detail-scherm haalt opnieuw op en slaat elders op. Kies één bron van waarheid (vaak de repository-laag) en laat elk scherm erdoor lezen.
Accountwisselingen zijn een frequente valkuil. Als iemand uitlogt of van account wisselt, wis user-scoped tabellen en keys. Anders kun je kort de vorige gebruiker’s profielfoto of orders tonen, wat als een privacy-incident aanvoelt.
Praktische oplossingen die bovenstaande problemen dekken:
Voorbeeld: je productlijst laadt direct uit cache en ververst stilletjes. Als verversen faalt, blijf je de gecachte data tonen maar maak duidelijk dat het mogelijk verouderd is en bied een Retry aan. Blokkeer de UI niet op verversen als gecachte data prima zou zijn.
Voordat je uitrolt, verander caching van “het lijkt goed” naar regels die je kunt testen. Gebruikers moeten data zien die logisch is, zelfs na heen en weer navigeren, offline gaan of inloggen met een andere account.
Voor elk scherm: bepaal hoe lang data als fresh mag gelden. Het kan minuten zijn voor snel bewegende data (berichten, saldi) of uren voor langzaam veranderende data (instellingen, productcategorieën). Bevestig daarna wat er gebeurt als het niet meer fresh is: achtergrondververs, ververs bij openen of handmatige pull-to-refresh.
Voor elk datatype: bepaal welke gebeurtenissen de cache moeten wissen of negeren. Veelgebruikte triggers: logout, het bewerken van het item, accountwisseling en app-updates die de datavorm veranderen.
Zorg dat gecachte items een klein pakket metadata naast de payload bewaren:
Houd eigenaarschap duidelijk: gebruik één repository per datatype (bijv. ProductsRepository), niet per widget. Widgets vragen data aan, ze mogen geen cacheregels bepalen.
Bepaal en test ook offline-gedrag. Bevestig welke schermen uit cache tonen, welke acties uitgeschakeld zijn en welke copy je toont (“Toont opgeslagen data”, plus een zichtbare ververscontrole). Handmatige verversing moet op elk cache-supported scherm aanwezig en makkelijk te vinden zijn.
Stel een simpele winkelapp voor met drie schermen: productcatalogus (lijst), productdetails en een Favorieten-tab. Gebruikers scrollen de catalogus, openen een product en tikken op het hartje om te favorieten. Het doel is snel aanvoelen, ook op trage netwerken, zonder verwarrende onverenigbaarheden.
Cache lokaal wat helpt direct te renderen: cataloguspagine (IDs, titel, prijs, thumbnail-URL, favorite-flag), productdetails (beschrijving, specificaties, beschikbaarheid, lastUpdated), beeldmetadata (URLs, sizes, cache-keys) en de favorieten van de gebruiker (een set product-IDs, optioneel met timestamps).
Wanneer de gebruiker de catalogus opent, toon direct gecachte resultaten en valideer op de achtergrond. Als verse data arriveert, update dan alleen wat veranderd is en houd de scrollpositie stabiel.
Voor de favorite-toggle behandel het als een “moet consistent zijn” actie. Update de lokale favorieten-set onmiddellijk (optimistische update), update vervolgens gecachte product-rijen en productdetails voor dat ID. Als de netwerkcall faalt, rol terug en toon een klein bericht.
Om navigatie consistent te houden, laat zowel lijstbadges als het hartje in detail vanuit dezelfde bron van waarheid komen (je lokale cache of store), niet uit aparte schermstaten. Het lijst-icoontje werkt meteen bij terugkeer, het detail-scherm reflecteert veranderingen die vanuit de lijst zijn gemaakt en het Favorieten-tab telt overal hetzelfde zonder op een refetch te wachten.
Voeg eenvoudige verversregels toe: catalogus-cache verloopt snel (minuten), productdetails iets langer en favorieten vervallen nooit maar worden altijd geconsolideerd na login/logout.
Caching is niet meer mysterieus als je team naar één pagina regels kan wijzen en het eens is over wat er gebeurt. Het doel is geen perfectie; het is voorspelbaar gedrag dat consistent blijft over releases.
Schrijf een klein tabelletje per scherm en houd het kort genoeg om tijdens wijzigingen te reviewen: schermnaam en hoofddatasoort, cache-locatie en sleutel, versheidsregel (TTL, event-based of handmatig), invalidatie-triggers en wat de gebruiker ziet tijdens verversen.
Voeg lichte logging toe terwijl je fijnstelt. Leg cache-hits, misses en waarom een verversing plaatsvond vast (TTL verlopen, gebruiker pull-to-refresh, app resumed, mutatie voltooid). Als iemand meldt “deze lijst voelt verkeerd”, maken die logs de bug oplosbaar.
Begin met simpele TTLs en verfijn op basis van wat gebruikers opmerken. Een nieuwsoverzicht accepteert misschien 5–10 minuten veroudering, terwijl een orderstatusscherm verversen bij resume en na elk checkout-act nodig heeft.
Als je snel een Flutter-app bouwt, helpt het om je datalaag en cache-regels uit te lijnen voordat je iets implementeert. Voor teams die Koder.ai gebruiken, is Planning Mode een handige plek om die per-scherm regels eerst te schrijven en daarna te bouwen.
Wanneer je verversgedrag afstelt, bescherm stabiele schermen terwijl je experimenteert. Snapshots en rollback besparen tijd als een nieuwe regel per ongeluk flicker, lege staten of inconsistente tellingen introduceert.
Begin met één duidelijke regel per scherm: wat het direct mag tonen (cache), wanneer het moet verversen en wat de gebruiker ziet tijdens verversen. Als je die regel niet in één zin kunt uitleggen, zal de app uiteindelijk inconsistent aanvoelen.
Behandel gecachte data alsof het een versheidsstatus heeft. Als het fresh is, toon het. Als het stale maar bruikbaar is, toon het nu en ververs stilletjes op de achtergrond. Als het moet verversen is, haal dan eerst nieuwe data op voordat je toont (of toon een loading/offline state). Dit zorgt voor consistente UI-gedragingen in plaats van “soms werkt het, soms niet.”
Cache dingen die vaak gelezen worden en waar wat ouderdom de gebruiker niet schaadt, zoals feeds, catalogi, referentiegegevens en basisprofielen. Wees voorzichtig met geld- of tijdkritische data zoals saldi, voorraad, ETAs en orderstatus; je kunt ze cachen voor snelheid, maar forceer een verversing vlak voor een besluitpunt of bevestiging.
Gebruik memory voor snelle hergebruik binnen de huidige sessie, zoals het huidige profiel of recent bekeken items. Gebruik disk key-value voor kleine, simpele items die restarts moeten overleven, zoals voorkeuren. Gebruik een lokale database wanneer data groot of gestructureerd is, je queries nodig hebt of offline support wilt, zoals berichten, orders of inventaris.
Een eenvoudige TTL is een goed startpunt: beschouw data als vers voor een vooraf ingestelde tijd en ververs daarna. Voor veel schermen is een betere ervaring: “toon direct uit cache, ververs op de achtergrond en update alleen als er iets is veranderd,” omdat dat lege schermen en veel flicker voorkomt.
Invalidate bij gebeurtenissen die duidelijk de betrouwbaarheid van de cache ondermijnen: gebruikerswijzigingen (create/update/delete), login/logout of accountwissel, app resume als data ouder is dan je TTL, en expliciete user refresh. Houd triggers klein en expliciet zodat je niet constant of juist nooit ververst.
Laat beide schermen uit dezelfde bron van waarheid lezen, niet uit privékopieën. Wanneer de gebruiker iets bewerkt op het detail-scherm, update dan het gedeelde gecachte object direct zodat de lijst bij terugkeer de nieuwe waarde toont. Sync daarna met de server en rol alleen terug als opslaan faalt.
Sla altijd metadata naast de payload op, vooral een timestamp en een gebruikersidentifier. Bij logout of accountwisseling moet je user-scoped cache-items onmiddellijk wissen of isoleren en in-flight requests voor de oude gebruiker annuleren zodat je niet kort de vorige gebruiker ziet.
Hou verouderde data zichtbaar en toon een kleine, duidelijke foutmelding met Retry-optie, in plaats van het scherm leeg te maken. Als een scherm oude data niet veilig kan tonen, gebruik dan een must-refresh-regel en toon een loading- of offline boodschap in plaats van doen alsof de oude waarde betrouwbaar is.
Leg cache-regels in je datalaag (bijv. repositories) zodat elk scherm hetzelfde gedrag volgt. Schrijf in Planning Mode eerst de per-scherm versheids- en invalidatieregels (bij gebruik van Koder.ai), en implementeer daarna zodat de UI simpelweg op staten reageert in plaats van zelf ververslogica te verzinnen.