Strategie buforowania w Flutterze: co przechowywać lokalnie, kiedy unieważniać dane i jak utrzymać spójność ekranów przez nawigację.

Cache w aplikacji mobilnej to przechowywanie kopii danych blisko (w pamięci lub na urządzeniu), dzięki czemu następny ekran może renderować się natychmiast, zamiast czekać na sieć. Te dane to może być lista elementów, profil użytkownika czy wyniki wyszukiwania.
Trudność polega na tym, że dane w cache często są lekko nieaktualne. Użytkownicy szybko to zauważą: cena, która się nie zaktualizowała, licznik powiadomień, który stoi w miejscu, albo ekran szczegółów pokazujący stare informacje zaraz po ich zmianie. Co utrudnia debugowanie, to timing. Ten sam endpoint może wyglądać dobrze po odświeżeniu pociągnięciem, ale błędnie po powrocie na poprzedni ekran, wznowieniu aplikacji lub przełączeniu konta.
Jest realny kompromis. Jeśli zawsze pobierasz świeże dane, ekrany będą wolne i „skakać”, a bateria i transfer szybciej się zużyją. Jeśli cacheujesz agresywnie, aplikacja będzie szybka, ale użytkownicy przestaną ufać tym danym.
Prosty cel pomaga: spraw, żeby świeżość była przewidywalna. Zdecyduj, co każdy ekran może pokazywać (świeże, lekko przestarzałe czy offline), jak długo dane mogą żyć przed odświeżeniem i jakie zdarzenia muszą je unieważnić.
Wyobraź sobie typowy przepływ: użytkownik otwiera zamówienie, potem wraca do listy zamówień. Jeśli lista pochodzi z cache, może nadal pokazywać stary status. Jeśli odświeżasz za każdym razem, lista będzie migać i sprawiać wrażenie wolnej. Jasne reguły, np. „pokaż cache natychmiast, odśwież w tle i zaktualizuj oba ekrany gdy przyjdzie odpowiedź”, czynią zachowanie spójnym przy nawigacji.
Cache to nie tylko „zapisane dane”. To kopia plus reguła mówiąca, kiedy ta kopia jest wciąż ważna. Jeśli zapiszesz tylko payload bez reguły, skończysz z dwoma wersjami rzeczywistości: jeden ekran pokaże nowe informacje, inny wczorajsze.
Praktyczny model to przypisanie każdego elementu cache do jednego z trzech stanów:
Takie ujęcie sprawia, że UI jest przewidywalne, bo reaguje zawsze tak samo na dany stan.
Reguły świeżości powinny opierać się na sygnałach, które potrafisz wytłumaczyć współpracownikowi. Typowe wybory to: wygaśnięcie czasowe (np. 5 minut), zmiana wersji (schematu lub aplikacji), akcja użytkownika (pull-to-refresh, submit, delete) albo wskazówka od serwera (ETag, last-updated, lub jawna odpowiedź „cache invalid”).
Przykład: ekran profilu ładuje zapisane dane użytkownika natychmiast. Jeśli są przestarzałe, ale używalne, pokazuje zapisane imię i awatar, a jednocześnie cicho odświeża. Jeśli użytkownik właśnie edytował profil, to moment „musisz odświeżyć”. Aplikacja powinna natychmiast zaktualizować cache, żeby wszystkie ekrany pozostały spójne.
Zdecyduj, kto odpowiada za te reguły. W większości aplikacji najlepszy domyśl to: warstwa danych (data layer) kontroluje świeżość i unieważnianie, UI po prostu reaguje (pokaż cache, pokaż ładowanie, pokaż błąd), a backend dostarcza wskazówek kiedy może. To zapobiega sytuacji, w której każdy ekran wymyśla własne reguły.
Dobry cache zaczyna się od jednego pytania: czy lekko nieaktualne dane zaszkodzą użytkownikowi? Jeśli odpowiedź brzmi „raczej nie”, to zwykle warto je cacheować.
Dane często czytane i rzadko zmieniane to dobry kandydat: feedy i listy, zawartość katalogowa (produkty, artykuły), dane referencyjne (kategorie, kraje). Ustawienia i preferencje też tu pasują, podobnie podstawowe informacje profilu jak imię i URL awatara.
Ryzykowna strona to wszystko, co dotyczy pieniędzy lub czasu. Salda, status płatności, dostępność magazynowa, terminy wizyt, ETA dostawy i „ostatnio widziany” mogą powodować realne problemy jeśli będą przestarzałe. Możesz je cacheować dla szybkości, ale traktuj cache jako tymczasowy placeholder i wymuś odświeżenie w punktach decyzyjnych (np. tuż przed potwierdzeniem zamówienia).
Stan UI wyprowadzony z danych to osobna kategoria. Zapisywanie wybranego taba, filtrów, zapytania wyszukiwania, sortowania czy pozycji przewijania może uczynić nawigację płynną. Może też zmylić użytkowników, gdy stare wybory pojawiają się niespodziewanie. Prosta reguła: przechowuj stan UI w pamięci, dopóki użytkownik pozostaje w danym flow, ale resetuj go, jeśli celowo „zaczyna od nowa” (np. powrót do ekranu głównego).
Unikaj cache’owania danych stwarzających ryzyko bezpieczeństwa lub prywatności: sekretów (hasła, klucze API), jednorazowych tokenów (kody OTP, tokeny resetujące hasło) oraz wrażliwych danych osobowych chyba że naprawdę potrzebujesz dostępu offline. Nigdy nie cache’uj pełnych danych karty ani niczego, co zwiększa ryzyko oszustwa.
W aplikacji zakupowej warto cache’ować listę produktów — to duża korzyść. Ekran checkout jednak powinien zawsze odświeżyć podsumowania i dostępność tuż przed zakupem.
Większość aplikacji Flutter potrzebuje lokalnego cache, żeby ekrany ładowały się szybko i nie migały puste podczas budzenia sieci. Kluczowa decyzja to miejsce przechowywania — każda warstwa ma inną prędkość, ograniczenia rozmiaru i zachowanie dotyczące czyszczenia.
Cache w pamięci jest najszybszy. Świetnie do danych, które właśnie pobrałeś i ponownie użyjesz, dopóki aplikacja jest otwarta: aktualny profil, ostatnie wyniki wyszukiwania, produkt właśnie oglądany. Minus: znika po zabiciu aplikacji, więc nie pomaga przy cold start lub offline.
Przechowywanie klucz-wartość na dysku pasuje do małych elementów, które mają przetrwać restarty. Myśl o preferencjach, „ostatnio wybranym tabie” i małych JSON-ach. Trzymaj to celowo małe — wkładanie dużych list do key-value szybko robi bałagan i puchnięcie.
Lokalna baza danych jest najlepsza, gdy dane są większe, ustrukturyzowane lub potrzebujesz zachowania offline. Pomaga też przy zapytaniach typu „wszystkie nieprzeczytane wiadomości”, „pozycje w koszyku”, „zamówienia z ostatniego miesiąca” zamiast ładować duży blob i filtrować w pamięci.
Aby zachować przewidywalność, wybierz jeden główny magazyn dla każdego typu danych i unikaj trzymania tego samego zbioru w trzech miejscach.
Krótka ściągawka:
Planuj też rozmiar. Zdecyduj co to znaczy „za duże”, jak długo trzymasz elementy i jak sprzątasz. Przykład: ogranicz zapis wyników wyszukiwania do ostatnich 20 zapytań i regularnie usuwaj rekordy starsze niż 30 dni, żeby cache się nie rozrastał w nieskończoność.
Reguły odświeżania powinny być na tyle proste, że wyjaśnisz je jednym zdaniem na ekran. Tu właśnie sensowne cache’owanie się opłaca: użytkownicy mają szybkie ekrany, a aplikacja pozostaje wiarygodna.
Najprostsza reguła to TTL (time to live). Zapisuj dane ze znacznikiem czasu i traktuj je jako świeże przez, powiedzmy, 5 minut. Potem stają się przestarzałe. TTL dobrze sprawdza się dla danych „miło mieć”, jak feed, kategorie czy rekomendacje.
Przydatne udoskonalenie to podział TTL na soft TTL i hard TTL.
Ze soft TTL pokazujesz cache natychmiast, potem odświeżasz w tle i aktualizujesz UI jeśli coś się zmieniło. Z hard TTL przestajesz pokazywać stare dane po wygaśnięciu — blokujesz ekran loaderem lub pokazujesz stan offline/try again. Hard TTL pasuje tam, gdzie bycie niepoprawnym jest gorsze niż wolne działanie, np. salda, status zamówienia, uprawnienia.
Jeśli backend to wspiera, preferuj „odśwież tylko gdy coś się zmieni” korzystając z ETag, updatedAt lub pola wersji. Aplikacja może zapytać „czy to się zmieniło?” i pominąć pobieranie pełnego payloadu gdy nic nowego nie ma.
Przyjazny użytkownikowi default to stale-while-revalidate: pokaż teraz, odśwież cicho i przerysuj tylko jeśli wynik się różni. Daje szybkość bez losowego migotania.
Często reguły per-ekran wyglądają tak:
Wybieraj reguły na podstawie kosztu bycia w błędzie, nie tylko kosztu pobrania.
Unieważnianie cache zaczyna się od pytania: jakie zdarzenie sprawia, że cache jest mniej godny zaufania niż koszt ponownego pobrania? Jeśli wybierzesz mały zestaw wyzwalaczy i ich się trzymasz, zachowanie będzie przewidywalne, a UI stabilne.
Wyzwalacze, które naprawdę się liczą w praktyce:
Przykład: użytkownik zmienia zdjęcie profilowe, potem wraca. Jeśli polegasz tylko na odświeżaniu czasowym, poprzedni ekran może pokazać stare zdjęcie do następnego fetchu. Zamiast tego potraktuj edycję jako wyzwalacz: zaktualizuj zbuforowany obiekt profilu od razu i oznacz go jako świeży z nowym timestampem.
Trzymaj reguły unieważniania małe i explicite. Jeśli nie potrafisz wskazać dokładnego zdarzenia, które unieważnia wpis cache, będziesz albo odświeżać zbyt często (powolne, skaczące UI), albo za rzadko (przestarzałe ekrany).
Zacznij od spisania kluczowych ekranów i danych, których każdy z nich potrzebuje. Nie myśl w endpointach. Myśl o obiektach widocznych dla użytkownika: profil, koszyk, lista zamówień, pozycja katalogu, licznik nieprzeczytanych.
Następnie wybierz jedno źródło prawdy na typ danych. W Flutterze zazwyczaj jest to repository, które ukrywa skąd pochodzą dane (pamięć, dysk, sieć). Ekrany nie powinny decydować, kiedy uderzać w sieć — powinny prosić repozytorium o dane i reagować na zwrócony stan.
Praktyczny przepływ:
Metadata to to, co czyni reguły wykonalnymi. Jeśli ownerUserId się zmieni (logout/login), możesz odrzucić lub zignorować stare wpisy natychmiast zamiast pokazywać dane poprzedniego użytkownika przez ułamek sekundy.
Dla zachowania UI, zdecyduj wcześniej co znaczy „przestarzałe”. Powszechna reguła: pokaż przestarzałe dane natychmiast, uruchom odświeżanie w tle i zaktualizuj gdy nadejdą nowe dane. Jeśli odświeżanie się nie uda, zostaw przestarzałe dane i pokaż mały, czytelny błąd.
Potem utrwal reguły kilkoma nudnymi testami:
To różnica między „mamy cache” a „nasza aplikacja zachowuje się tak samo za każdym razem.”
Nic nie niszczy zaufania szybciej niż zobaczyć jedną wartość na liście, wejść w szczegóły, edytować ją, a potem po powrocie widzieć starą wartość. Spójność przy nawigacji bierze się z tego, że każdy ekran czyta z tego samego źródła.
Zasada: pobierz raz, zapisz raz, renderuj wiele razy. Ekrany nie powinny niezależnie wywoływać tego samego endpointu i trzymać prywatnych kopii. Trzymaj dane w współdzielonym store (warstwa stanu), a lista i ekran szczegółów niech obserwują te same dane.
Miej jedno miejsce odpowiedzialne za aktualną wartość i świeżość. Ekrany mogą prosić o odświeżenie, ale nie powinny zarządzać własnymi timerami, retry czy parsowaniem.
Praktyczne nawyki, które zapobiegają „dwóm wersjom rzeczywistości”:
Nawet z dobrymi regułami użytkownicy czasem zobaczą przestarzałe dane (offline, wolna sieć, aplikacja w tle). Zasygnalizuj to subtelnie: „Zaktualizowano przed chwilą” z timestampem, delikatny indicator „Odświeżanie…”, albo odznaka „Offline”.
Dla edycji optymistyczne aktualizacje często działają najlepiej. Przykład: użytkownik zmienia cenę produktu na ekranie szczegółów. Natychmiast zaktualizuj współdzielony store, żeby lista po powrocie pokazywała nową cenę. Jeśli zapis się nie powiedzie, cofnij zmianę i pokaż krótki komunikat o błędzie.
Większość awarii cache jest nudna: cache działa, ale nikt nie potrafi powiedzieć, kiedy go używać, kiedy wygasa i kto za niego odpowiada.
Pierwsza pułapka to cache bez metadanych. Jeśli zapiszesz tylko payload, nie dowiesz się czy jest stary, jaka wersja app/schematu go stworzyła ani do którego użytkownika należy. Zapisz przynajmniej savedAt, prosty numer wersji i userId. Ten nawyk zapobiega wielu „dlaczego ekran jest nieprawidłowy?” bugom.
Inny problem to wiele cache’ów dla tych samych danych bez właściciela. Ekran listy trzyma in-memory listę, repozytorium zapisuje na dysku, ekran szczegółów pobiera i zapisuje gdzie indziej. Wybierz jedno źródło prawdy (zazwyczaj repozytorium) i spraw, by każdy ekran czytał przez nie.
Zmiany konta to częsty błąd. Jeśli ktoś się wyloguje lub przełączy konto, wyczyść tabele i klucze związane z użytkownikiem. W przeciwnym razie możesz przez chwilę pokazać poprzedniego użytkownika — to wygląda jak naruszenie prywatności.
Praktyczne poprawki:
Przykład: lista produktów ładuje się błyskawicznie z cache, potem cicho odświeża. Jeśli odświeżenie się nie powiedzie, pokaż cache z informacją, że może być nieaktualny i daj Retry. Nie blokuj UI odświeżaniem, gdy cache jest akceptowalny.
Przed release zamień cache z „wydaje się ok” w zestaw reguł, które da się testować. Użytkownicy powinni widzieć sensowne dane nawet po nawigacji w przód i w tył, pójściu offline lub zalogowaniu na inne konto.
Dla każdego ekranu zdecyduj, jak długo dane mogą być uważane za świeże. To mogą być minuty dla szybko zmieniających się danych (wiadomości, salda) lub godziny dla powolnych (ustawienia, kategorie produktów). Potem potwierdź, co się dzieje gdy dane są nieświeże: odświeżenie w tle, odśwież przy otwarciu, czy manualne pull-to-refresh.
Dla każdego typu danych zdecyduj, jakie zdarzenia muszą wyczyścić lub pominąć cache. Typowe wyzwalacze: logout, edycja elementu, przełączenie konta, aktualizacja aplikacji zmieniająca kształt danych.
Upewnij się, że wpisy cache mają mały zestaw metadanych obok payloadu:
Trzymaj właściciela danych jasny: jedno repozytorium na typ danych (np. ProductsRepository), nie jedno na widget. Widgety proszą o dane, nie wymyślają reguł cache.
Zadecyduj też i przetestuj zachowanie offline. Ustal, co ekrany pokazują z cache, które akcje są wyłączone i jaki tekst wyświetlasz („Pokazywane zapisane dane” oraz widoczny control do odświeżenia). Manualne odświeżenie powinno być w każdym ekranie opartym na cache i łatwe do znalezienia.
Wyobraź sobie prostą aplikację sklepową z trzema ekranami: katalog produktów (lista), szczegóły produktu i zakładka Ulubione. Użytkownicy przewijają katalog, otwierają produkt i klepią ikonę serca, by dodać do ulubionych. Cel: szybkość nawet przy wolnej sieci, bez mylących niezgodności.
Cacheuj lokalnie to, co pomaga renderować od razu: strony katalogu (ID, tytuł, cena, URL miniaturki, flaga ulubionych), szczegóły produktu (opis, specyfikacja, dostępność, lastUpdated), metadane obrazów (URL, rozmiary, klucze cache) oraz zbiór ulubionych (set ID produktów, opcjonalnie z timestampami).
Gdy użytkownik otwiera katalog, pokaż zapisane wyniki natychmiast, potem rewaliduj w tle. Jeśli nadejdą świeże dane, zaktualizuj tylko to, co się zmieniło i zachowaj pozycję przewijania.
Przełącznik ulubionych traktuj jako akcję „musi być spójna”. Zrób aktualizację lokalnego zbioru ulubionych od razu (optymistycznie), potem zaktualizuj zbuforowane wiersze produktu i zbuforowane szczegóły dla tego ID. Jeśli wywołanie sieciowe się nie powiedzie, wycofaj i pokaż krótki komunikat.
Aby zachować spójność nawigacji, napędzaj zarówno odznaki na liście, jak i ikonę serca w szczegółach z tego samego źródła prawdy (lokalny cache lub store), nie z oddzielnego stanu widgetu. Serce na liście aktualizuje się natychmiast po powrocie z szczegółów, ekran szczegółów odzwierciedla zmiany z listy, a licznik we wszystkich miejscach zgadza się bez oczekiwania na ponowne pobranie.
Dodaj proste reguły odświeżania: cache katalogu wygasa szybko (minuty), szczegóły produktu trochę dłużej, a ulubione nigdy nie wygasają, ale zawsze się rekoncyliują po login/logout.
Cache przestaje być tajemnicą, gdy zespół może wskazać jedną stronę reguł i zgodzić się, co się stanie. Cel to nie perfekcja, lecz przewidywalne zachowanie, które utrzyma się przez kolejne wydania.
Napisz krótką tabelę per ekran, tak krótką, żeby dało się ją przejrzeć przy zmianach: nazwa ekranu i główne dane, lokalizacja cache i klucz, reguła świeżości (TTL, zdarzeniowa lub manualna), wyzwalacze invalidacji i co użytkownik widzi podczas odświeżania.
Dodaj lekkie logowanie podczas strojenia. Rejestruj trafienia cache, pudła i powód odświeżenia (TTL wygasł, użytkownik pociągnął do odświeżenia, aplikacja wznowiła się, mutation zakończone). Gdy ktoś zgłosi „ta lista wygląda źle”, takie logi czynią błąd rozwiązywalnym.
Zacznij od prostych TTL, potem dopracuj według tego, co zauważają użytkownicy. Feed informacyjny może akceptować 5–10 minut stalenia, ekran statusu zamówienia może potrzebować odświeżenia przy wznowieniu i po każdej akcji checkout.
Jeśli szybko budujesz aplikację Flutter, warto wcześniej naszkicować warstwę danych i reguły cache. Dla zespołów korzystających z Koder.ai (koder.ai) tryb planowania to praktyczne miejsce, by najpierw spisać reguły per-ekran, a potem budować zgodnie z nimi.
Podczas strojenia zachowania odświeżania, chroń stabilne ekrany podczas eksperymentów. Snapshoty i rollback oszczędzą czas, gdy nowa reguła przypadkowo wprowadzi migotanie, puste stany lub niespójne liczniki przy nawigacji.
Zacznij od jednej jasnej zasady na ekran: co może pokazywać natychmiast (z cache), kiedy musi odświeżyć dane i co widzi użytkownik podczas odświeżania. Jeśli nie potrafisz wyjaśnić tej zasady jednym zdaniem, aplikacja prędzej czy później będzie zachowywać się niespójnie.
Traktuj dane w cache jak mające stan świeżości. Jeśli są świeże, pokaż je. Jeśli są przestarzałe, ale używalne, pokaż je teraz i odśwież w tle. Jeśli wymagają odświeżenia, pobierz je przed pokazaniem (lub pokaż stan ładowania/wyłączony). Dzięki temu UI zachowuje się spójnie zamiast „czasami się aktualizować, czasami nie”.
Cacheuj dane, które są często czytane i mogą być nieco nieaktualne bez szkody: feedy, katalogi, dane referencyjne i podstawowe informacje o profilu. Ostrożnie podchodź do danych finansowych i krytycznych czasowo (salda, dostępność, ETA dostawy) — możesz je cacheować dla szybkości, ale wymuś odświeżenie przed podjęciem decyzji (np. przed potwierdzeniem zamówienia).
Pamięć (memory) dla szybkiego ponownego użycia w trakcie sesji, dysk (key-value) dla małych elementów które mają przetrwać restart (preferencje), baza lokalna kiedy dane są duże, ustrukturyzowane lub muszą działać offline (wiadomości, zamówienia, inwentarz).
Zwykły TTL to dobry domyślny wybór: uznaj dane za świeże przez pewien czas, potem odśwież. Dla lepszego UX preferuj „show cached now, refresh in background, update if changed” — unika pustych ekranów i zmniejsza migotanie.
Inwaliduj cache przy zdarzeniach, które naprawdę zmieniają zaufanie do danych: edycje użytkownika (create/update/delete), wylogowanie/przełączenie konta, wznowienie z tła jeśli dane są starsze niż TTL oraz explicite odświeżenie przez użytkownika. Utrzymuj te wyzwalacze małe i konkretne, żeby nie odświeżać ciągle ani nigdy wtedy, gdy trzeba.
Spraw, by oba ekrany czytały z tej samej pojedynczej prawdy (source of truth), nie z prywatnych kopii. Po edycji na ekranie szczegółów natychmiast zaktualizuj współdzielony obiekt w cache, aby lista po powrocie od razu pokazywała nową wartość, a następnie zsynchronizuj z serwerem i cofnij zmianę tylko jeśli zapis nie powiedzie się.
Zawsze zapisuj metadata obok payloadu, szczególnie znacznik czasu i identyfikator użytkownika. Przy wylogowaniu lub przełączeniu konta natychmiast czyść lub izoluj wpisy zakresu użytkownika i anuluj trwające żądania związane ze starym użytkownikiem, żeby nie pokazać czyjegoś zdjęcia profilowego przez ułamek sekundy.
Domyślnie zachowaj przestarzałe dane widoczne i pokaż mały, czytelny komunikat z możliwością ponowienia zamiast czyścić ekran. Jeśli ekran nie może bezpiecznie pokazywać starych danych — użyj reguły "must-refresh" i pokaż stan ładowania lub komunikat offline zamiast udawać, że przestarzała wartość jest wiarygodna.
Logika cache powinna żyć w warstwie danych (np. w repozytoriach), żeby każdy ekran stosował te same zasady. Najpierw zaplanuj reguły per-ekran w trybie planowania (Planning Mode) — potem UI tylko reaguje na stany zamiast wymyślać własne reguły odświeżania.