Strategie di caching Flutter per cache locale, dati obsoleti e regole di aggiornamento: cosa memorizzare, quando invalidare e come mantenere le schermate coerenti.

Il caching in un'app mobile significa tenere una copia dei dati vicino (in memoria o sul dispositivo) così la schermata successiva può renderizzare all'istante invece di aspettare la rete. Questi dati possono essere una lista di elementi, il profilo utente o i risultati di una ricerca.
La parte difficile è che i dati in cache sono spesso leggermente sbagliati. Gli utenti lo notano in fretta: un prezzo che non si aggiorna, un contatore di badge che sembra bloccato o una schermata dettagli che mostra informazioni vecchie subito dopo che sono state modificate. Ciò che rende il debug doloroso è il timing. Lo stesso endpoint può sembrare a posto dopo un pull-to-refresh, ma sbagliato dopo un ritorno indietro, il resume dell'app o il cambio account.
C'è un vero compromesso. Se fetchi sempre dati freschi, le schermate sembrano lente e scattose e consumi batteria e dati. Se fai cache aggressiva, l'app sembra veloce ma le persone smettono di fidarsi di quello che vedono.
Un obiettivo semplice aiuta: rendere la freschezza prevedibile. Decidi cosa ogni schermata è autorizzata a mostrare (fresco, leggermente obsoleto o offline), per quanto tempo i dati possono restare prima di essere aggiornati e quali eventi devono invalidarli.
Immagina un flusso comune: un utente apre un ordine e poi torna alla lista ordini. Se la lista proviene dalla cache, potrebbe ancora mostrare lo stato vecchio. Se aggiorni sempre, la lista può sfarfallare e sembrare lenta. Regole chiare come “mostra la cache subito, aggiorna in background e aggiorna entrambe le schermate quando arriva la risposta” rendono l'esperienza coerente durante la navigazione.
Una cache non è solo “dati salvati”. È una copia salvata più una regola su quando quella copia è ancora valida. Se memorizzi il payload ma salti la regola, ti ritrovi con due versioni della realtà: una schermata mostra informazioni nuove, un'altra quelle di ieri.
Un modello pratico è mettere ogni elemento in cache in uno di tre stati:
Questo inquadramento mantiene l'interfaccia prevedibile perché può rispondere sempre nello stesso modo a un dato stato.
Le regole di freschezza dovrebbero basarsi su segnali che puoi spiegare a un collega. Scelte comuni sono: scadenza temporale (ad es. 5 minuti), cambio di versione (schema o versione app), azione dell'utente (pull to refresh, submit, delete) o un suggerimento dal server (ETag, timestamp last-updated o una risposta esplicita di "cache invalid").
Esempio: una schermata profilo carica subito i dati utente in cache. Se sono stale-ma-utilizzabili, mostra nome e avatar memorizzati e poi aggiorna silenziosamente. Se l'utente ha appena modificato il profilo, quello è un momento di must-refresh. L'app dovrebbe aggiornare la cache immediatamente così ogni schermata resta coerente.
Decidi chi possiede queste regole. Nella maggior parte delle app, il miglior default è: il data layer possiede freschezza e invalidazione, la UI reagisce (mostra cache, mostra caricamento, mostra errore) e il backend fornisce suggerimenti quando può. Questo evita che ogni schermata inventi le proprie regole.
Un buon caching parte da una domanda: se questi dati sono un po' vecchi, danneggerà l'utente? Se la risposta è “probabilmente no”, di solito è adatto alla cache locale.
I dati letti spesso e che cambiano lentamente valgono in genere la pena di essere cachati: feed e liste che le persone scorrono spesso, contenuti in stile catalogo (prodotti, articoli, template) e dati di riferimento come categorie o paesi. Impostazioni e preferenze rientrano qui, insieme a informazioni profilo di base come nome e URL avatar.
Il lato rischioso è tutto ciò che riguarda soldi o tempo critico. Saldi, stato di pagamento, disponibilità di stock, slot appuntamenti, ETA di consegna e “ultimo accesso” possono causare problemi reali se sono obsoleti. Puoi comunque cacharli per velocità, ma tratta la cache come un segnaposto temporaneo e forza un refresh nei punti di decisione (ad esempio, subito prima di confermare un ordine).
Lo stato UI derivato è una categoria a sé. Salvare la tab selezionata, filtri, query di ricerca, ordine di ordinamento o posizione di scorrimento può rendere la navigazione fluida. Può anche confondere se scelte vecchie riappaiono inaspettatamente. Una regola semplice funziona bene: conserva lo stato UI in memoria mentre l'utente rimane nel flusso, ma resettalo quando l'utente “ricomincia” intenzionalmente (per esempio tornando alla home).
Evita di cachare dati che creano rischi di sicurezza o privacy: segreti (password, API key), token monouso (OTP, reset password) e dati personali sensibili a meno che non serva veramente l'accesso offline. Non memorizzare mai dettagli completi di carta o qualsiasi cosa che aumenti il rischio di frode.
In un'app di shopping, la cache della lista prodotti è una grande vittoria. La schermata checkout, invece, dovrebbe sempre aggiornare totali e disponibilità subito prima dell'acquisto.
La maggior parte delle app Flutter finisce per aver bisogno di una cache locale così le schermate si caricano velocemente e non appaiono vuote mentre la rete si risveglia. La decisione chiave è dove mettere i dati in cache, perché ogni layer ha diversa velocità, limiti di dimensione e comportamento di pulizia.
Una cache in memoria è la più veloce. È ottima per dati appena fetchati e riutilizzati mentre l'app resta aperta, come il profilo utente corrente, gli ultimi risultati di ricerca o un prodotto appena visualizzato. Il compromesso è semplice: scompare quando l'app viene uccisa, quindi non aiuta cold start o uso offline.
Lo storage key-value su disco va bene per piccole voci che vuoi conservare tra riavvii. Pensa a preferenze e blob piccoli: feature flag, “ultima tab selezionata” e piccole risposte JSON che cambiano raramente. Tienilo intenzionalmente piccolo. Quando inizi a mettere liste grandi nello storage key-value, gli aggiornamenti diventano confusi e aumenta facilmente il bloat.
Un database locale è la scelta migliore quando i dati sono più grandi, strutturati o necessitano comportamento offline. Aiuta anche quando servono query (“tutti i messaggi non letti”, “elementi nel carrello”, “ordini dell'ultimo mese”) invece di caricare un grande blob e filtrare in memoria.
Per mantenere il caching prevedibile, scegli uno store primario per ogni tipo di dato ed evita di tenere lo stesso dataset in tre posti.
Una regola rapida:
Pianifica anche le dimensioni. Decidi cosa significa “troppo grande”, per quanto tempo tieni le voci e come le pulisci. Per esempio: limita i risultati di ricerca cache agli ultimi 20 query e rimuovi regolarmente record più vecchi di 30 giorni così la cache non cresce silenziosamente all'infinito.
Le regole di refresh dovrebbero essere abbastanza semplici da poterle spiegare in una frase per schermata. È qui che il caching sensato ripaga: gli utenti ottengono schermate veloci e l'app resta affidabile.
La regola più semplice è TTL (time to live). Salva i dati con un timestamp e trattali come freschi per, diciamo, 5 minuti. Dopo diventano obsoleti. TTL funziona bene per dati “carini da avere” come un feed, categorie o raccomandazioni.
Un affinamento utile è dividere il TTL in soft TTL e hard TTL.
Con un soft TTL mostri subito la cache, poi aggiorni in background e aggiornando l'interfaccia se è cambiato. Con un hard TTL smetti di mostrare i dati vecchi una volta scaduti. Blocchi con un loader o mostri uno stato “offline/ritenta”. L'hard TTL si adatta ai casi in cui sbagliare è peggio che essere lenti, come saldi, stato ordini o permessi.
Se il tuo backend lo supporta, preferisci “aggiorna solo se cambiato” usando ETag, updatedAt o un campo version. L'app può chiedere “è cambiato?” e saltare il download del payload completo quando non c'è nulla di nuovo.
Un default user-friendly per molte schermate è stale-while-revalidate: mostra ora, aggiorna silenziosamente e ridisegna solo se il risultato differisce. Offre velocità senza flicker casuale.
La freschezza per schermata spesso diventa così:
Scegli le regole basandoti sul costo di essere in errore, non solo sul costo del fetch.
L'invalidazione della cache comincia con una domanda: quale evento rende i dati in cache meno affidabili del costo di rifetcharli? Se scegli un piccolo insieme di trigger e ti attieni a essi, il comportamento resta prevedibile e l'interfaccia appare stabile.
I trigger che contano di più nelle app reali:
Esempio: un utente modifica la foto del profilo, poi torna indietro. Se ti basi solo su refresh temporali, la schermata precedente potrebbe mostrare l'immagine vecchia fino al prossimo fetch. Tratta la modifica come trigger: aggiorna l'oggetto profilo in cache subito e marcane la freschezza con un nuovo timestamp.
Mantieni le regole di invalidazione piccole ed esplicite. Se non riesci a indicare l'evento esatto che invalida una voce di cache, finirai per rinfrescare troppo spesso (UI lenta e scattosa) o troppo poco (schermate obsolete).
Inizia elencando le schermate chiave e i dati di cui ogni schermata ha bisogno. Non pensare in endpoint. Pensa in oggetti visibili dall'utente: profilo, carrello, lista ordini, dettaglio catalogo, conteggio non letti.
Poi scegli una source of truth per ogni tipo di dato. In Flutter, di solito è un repository che nasconde da dove arrivano i dati (memoria, disco, rete). Le schermate non dovrebbero decidere quando colpire la rete. Dovrebbero chiedere il dato al repository e reagire allo stato restituito.
Un flusso pratico:
I metadata rendono le regole applicabili. Se ownerUserId cambia (logout/login), puoi eliminare o ignorare le righe cache vecchie immediatamente invece di mostrare i dati del precedente utente per mezzo secondo.
Per il comportamento UI, decidi in anticipo cosa significa “stale”. Una regola comune: mostra subito i dati obsoleti così la schermata non è vuota, avvia un refresh in background e aggiorna quando arrivano dati nuovi. Se il refresh fallisce, tieni visibili i dati obsoleti e mostra un piccolo errore chiaro.
Poi fissa le regole con alcuni test noiosi:
Questa è la differenza tra “abbiamo caching” e “la nostra app si comporta allo stesso modo ogni volta”.
Niente rompe la fiducia più del vedere un valore nella lista, aprire i dettagli, modificarlo e tornare indietro vedendo il valore vecchio. La coerenza tra schermate nasce dal fatto che ogni schermata legga dalla stessa fonte.
Una regola solida è: fetcha una volta, salva una volta, rendi molte volte. Le schermate non dovrebbero chiamare lo stesso endpoint indipendentemente e tenere copie private. Metti i dati in cache in uno store condiviso (il tuo layer di state management) e lascia che sia la lista che il dettaglio ad osservare gli stessi dati.
Tieni un unico luogo che possiede il valore corrente e la freschezza. Le schermate possono richiedere un refresh, ma non devono gestire timer, retry e parsing singolarmente.
Abitudini pratiche che prevengono “due versioni della realtà”:
Anche con buone regole, gli utenti vedranno a volte dati obsoleti (offline, rete lenta, app in background). Rendilo evidente con segnali piccoli e calmi: un timestamp “Aggiornato ora”, un indicatore sottile “Refreshing…” o un badge “Offline”.
Per le modifiche, gli aggiornamenti ottimistici spesso funzionano meglio. Esempio: l'utente cambia il prezzo di un prodotto nella schermata dettaglio. Aggiorna subito lo store condiviso così la lista mostra il nuovo prezzo al ritorno. Se il salvataggio fallisce, torna indietro al valore precedente e mostra un errore breve.
La maggior parte dei fallimenti di caching è banale: la cache funziona, ma nessuno sa dire quando usarla, quando scade e chi la possiede.
La prima trappola è cachare senza metadata. Se salvi solo il payload, non puoi sapere se è vecchio, quale versione dell'app lo ha prodotto o a quale utente appartiene. Salva almeno savedAt, un semplice numero di versione e userId. Questa abitudine previene molti bug tipo “perché questa schermata è sbagliata?”.
Un altro problema comune è avere cache multiple per lo stesso dato senza un owner. Una schermata lista tiene una lista in memoria, un repository scrive su disco e una schermata dettagli fetcha e salva altrove. Scegli una source of truth (spesso il repository) e fai sì che tutte le schermate leggano attraverso essa.
I cambi account sono un frequente colpo basso. Se qualcuno effettua logout o cambia account, cancella tabelle e chiavi user-scoped. Altrimenti potresti mostrare per un attimo foto profilo o ordini del precedente utente, che sembra una violazione della privacy.
Fix pratici che coprono i problemi sopra:
Esempio: la tua lista prodotti carica istantaneamente dalla cache, poi aggiorna silenziosamente. Se il refresh fallisce, continua a mostrare i dati cached ma indica chiaramente che potrebbero essere obsoleti e offri Retry. Non bloccare la UI sul refresh quando i dati in cache vanno bene.
Prima della release, trasforma il caching da “sembra a posto” in regole testabili. Gli utenti dovrebbero vedere dati sensati anche dopo aver navigato avanti e indietro, essere offline o aver effettuato l'accesso con un account diverso.
Per ogni schermata, decidi per quanto tempo i dati sono considerati freschi. Possono essere minuti per dati veloci (messaggi, saldi) o ore per dati lenti (impostazioni, categorie prodotto). Poi conferma cosa succede quando non lo sono: refresh in background, refresh all'apertura o pull-to-refresh manuale.
Per ogni tipo di dato, decidi quali eventi devono bypassare o cancellare la cache. Trigger comuni includono logout, modifica dell'item, cambio account e aggiornamenti dell'app che cambiano la forma dei dati.
Assicurati che ogni voce in cache salvi accanto al payload un piccolo set di metadata:
Mantieni chiara la proprietà: usa un repository per tipo di dato (es. ProductsRepository), non uno per widget. I widget dovrebbero chiedere dati, non decidere le regole di cache.
Decidi e testa anche il comportamento offline. Conferma cosa mostrano le schermate dalla cache, quali azioni sono disabilitate e quale copy visualizzi (“Mostrando dati salvati”, più un controllo di refresh visibile). Il refresh manuale dovrebbe esistere su ogni schermata supportata da cache ed essere facile da trovare.
Immagina una semplice app shop con tre schermate: catalogo prodotti (lista), dettaglio prodotto e tab Preferiti. Gli utenti scorrono il catalogo, aprono un prodotto e toccano l'icona a cuore per aggiungerlo ai preferiti. L'obiettivo è dare sensazione di velocità anche su reti lente, senza mostrare mismatch confusi.
Metti in cache localmente ciò che aiuta il rendering istantaneo: pagine catalogo (ID, titolo, prezzo, thumbnail URL, flag favorito), dettagli prodotto (descrizione, specifiche, disponibilità, lastUpdated), metadata delle immagini (URL, dimensioni, cache key) e la lista preferiti dell'utente (insieme di product ID, opzionalmente con timestamp).
Quando l'utente apre il catalogo, mostra subito i risultati dalla cache e poi riconvalida in background. Se arrivano dati freschi, aggiorna solo ciò che è cambiato mantenendo stabile la posizione di scroll.
Per il toggle preferito, trattalo come un'azione “deve essere consistente”. Aggiorna subito il set locale dei preferiti (update ottimistico), poi aggiorna le righe prodotto in cache e i dettagli prodotto per quell'ID. Se la chiamata di rete fallisce, fai il rollback e mostra un piccolo messaggio.
Per mantenere coerenza nella navigazione, guida badge della lista e icona cuore in dettaglio dalla stessa source of truth (la tua cache locale o store), non dallo stato privato delle schermate. Il cuore nella lista si aggiorna appena torni dai dettagli, il dettaglio riflette cambi fatti dalla lista e il conteggio nella tab Preferiti corrisponde ovunque senza aspettare un refetch.
Aggiungi regole di refresh semplici: la cache del catalogo scade rapidamente (minuti), i dettagli prodotto un po' più tardi e i preferiti non scadono mai ma si riconciliano sempre dopo login/logout.
Il caching smette di essere misterioso quando il team può puntare a una pagina di regole e accordarsi su cosa accade. L'obiettivo non è la perfezione, ma un comportamento prevedibile che resti lo stesso tra le release.
Scrivi una piccola tabella per schermata e tienila breve: nome schermata e dato principale, posizione della cache e chiave, regola di freschezza (TTL, basata su eventi o manuale), trigger di invalidazione e cosa vede l'utente durante il refresh.
Aggiungi logging leggero mentre affini. Registra hit e miss della cache e perché è avvenuto un refresh (TTL scaduto, pull-to-refresh, app resumed, mutazione completata). Quando qualcuno segnala “questa lista sembra sbagliata”, quei log rendono il bug risolvibile.
Inizia con TTL semplici, poi affina basandoti su ciò che notano gli utenti. Un feed di notizie può accettare 5–10 minuti di staleness, mentre una schermata di stato ordine potrebbe richiedere refresh al resume e dopo qualsiasi azione di checkout.
Se stai costruendo un'app Flutter rapidamente, può essere utile delineare il data layer e le regole di cache prima di implementare nulla. Per i team che usano Koder.ai (koder.ai), la planning mode è un buon posto per scrivere prima quelle regole per schermata, poi costruire per soddisfarle.
Quando affini il comportamento di refresh, proteggi le schermate stabili mentre sperimenti. Snapshot e rollback possono farti risparmiare tempo quando una nuova regola introduce flicker, stati vuoti o conteggi incoerenti durante la navigazione.
Inizia con una regola chiara per ogni schermata: cosa può mostrare immediatamente (cache), quando deve aggiornare e cosa vede l'utente durante l'aggiornamento. Se non riesci a spiegare la regola in una frase, l'app finirà per sembrare incoerente.
Tratta i dati in cache come dotati di uno stato di freschezza. Se è fresco, mostralo. Se è stale ma utilizzabile, mostralo subito e aggiornalo in background. Se è da aggiornare obbligatoriamente, effettua il fetch prima di mostrare (o mostra uno stato di caricamento/offline). Questo mantiene il comportamento UI coerente invece di avere aggiornamenti sporadici.
Cache ciò che viene letto spesso e può essere un po' vecchio senza recare danno: feed, cataloghi, dati di riferimento e informazioni di profilo di base. Evita o tratta con attenzione dati critici per soldi o tempo (saldo, disponibilità scorte, ETA, stato ordine): puoi cacharli per velocità, ma forza un refresh prima di qualsiasi decisione o conferma.
Usa la memoria per il riuso veloce nella sessione corrente (profilo attuale, elementi visti di recente). Usa lo storage key-value su disco per piccole voci persistenti tra riavvii (preferenze). Usa un database locale quando i dati sono grandi, strutturati, necessitano di query o devono funzionare offline (messaggi, ordini, inventario).
Un TTL semplice è un buon punto di partenza: considera i dati freschi per un tempo prefissato e poi aggiorna. Per molte schermate, un'esperienza migliore è “mostra la cache ora, aggiorna in background e ridisegna se cambia”, perché evita schermate vuote e riduce il flicker.
Invalidare sugli eventi che cambiano realmente l'affidabilità della cache: modifiche dell'utente (create/update/delete), login/logout o cambio account, resume dell'app se i dati sono più vecchi del TTL e refresh espliciti dell'utente. Mantieni questi trigger piccoli ed espliciti per non rinfrescare sempre o mai quando serve.
Fai sì che entrambe le schermate leggano dalla stessa source of truth, non da copie private. Quando l'utente modifica qualcosa nella schermata dettagli, aggiorna immediatamente l'oggetto cache condiviso così la lista mostrerà il nuovo valore al ritorno, poi sincronizza col server e rollback solo in caso di fallimento.
Conserva metadati accanto al payload, in particolare un timestamp e un identificatore utente. Al logout o al cambio account, cancella o isola immediatamente le voci di cache legate all'utente e annulla le richieste in corso legate al vecchio utente per evitare di renderizzare per un istante i suoi dati.
Di norma, mantieni i dati obsoleti visibili e mostra un piccolo stato d'errore con possibilità di riprovare, invece di lasciare la schermata vuota. Se però la schermata non può mostrare dati vecchi in sicurezza, passa a una regola di must-refresh e mostra un messaggio di caricamento o offline invece di fingere che il valore obsoleto sia attendibile.
Metti la logica di cache nel tuo data layer (per esempio repository) così ogni schermata segue lo stesso comportamento. Se lavori rapidamente con Koder.ai, definisci prima le regole di freschezza e invalidazione in planning mode, poi implementa in modo che la UI reagisca solo agli stati invece di inventare logiche di refresh.