flutter mobil uygulama geliştirme projelerinde HTTP önbelleğini Cache-Control, ETag ve stale-while-revalidate ile tasarlayın. Dio katmanında kullanıcı izolasyonu, 304 işleme ve ölçülebilir gecikme hedeflerini uygulayın.
Flutter Mobil Uygulama Geliştirmede HTTP Önbellek Tutarlılığı
Flutter mobil uygulama geliştirme için HTTP cache sözleşmesi
Bir flutter eğitimi veya flutter kursu sırasında çoğunlukla atlanan nokta, istemci önbelleğinin yalnızca bir MapCache-Control: private, max-age=60, stale-while-revalidate=300, yanıtın aynı kullanıcı kapsamındaki yerel depoda 60 saniye taze, sonraki 300 saniyede ise arka planda doğrulanarak gösterilebilir olduğunu ifade eder. no-store taşıyan ödeme, oturum veya tek kullanımlık imzalı URL yanıtlarını disk belleğine hiç yazmayın.
- Taze yanıt: now < storedAt + max-age. Ağ isteği yapmadan gövdeyi döndürün.
- Bayat ama kullanılabilir yanıt: stale-while-revalidate penceresindeyse gövdeyi hemen gösterin ve tek bir arka plan doğrulaması başlatın.
- Süresi geçmiş yanıt: ETag varsa
If-None-Matchile koşullu GET yapın. Sunucu 304 dönerse eski gövdeyi koruyup son kullanma zamanını yeni başlıklara göre hesaplayın. - Vary:
Accept-Languagevarsa Türkçe ve İngilizce içeriği aynı anahtara koymayın.Authorizationveya kullanıcı kimliği etkiliyse ham bearer token'ı anahtara yazmak yerine uygulama içi accountId kapsamı kullanın.
Dart'ın HttpClient katmanı, mobilde uygulamanız adına RFC uyumlu kalıcı bir HTTP cache kurmaz; Dio da varsayılan olarak GET yanıtlarını kalıcı önbelleğe almaz. Bu nedenle davranışı açıkça uygulamanız gerekir. Sunucu ekibiyle endpoint bazlı bir sözleşme çıkarın: örneğin ürün kataloğu için private, max-age=300, stale-while-revalidate=86400, kullanıcı bakiyesi için no-store, profil fotoğrafı için ETag ve uzun max-age. Bu ayrım, hassas verinin yanlış kullanıcı oturumunda görünmesini engelleyen mekanizmadır.
Dio ile ETag ve 304 yanıtlarını doğru işlemek
Aşağıdaki interceptor, taze bir girdiyi doğrudan döndürür; bayat girdinin ETag değerini koşullu GET'e ekler; 304 geldiğinde eski gövdeyi yeniden kullanır. Kritik ayrıntı şudur: Dio'nun varsayılan durum doğrulaması 304'ü hata akışına gönderebilir. Bu yüzden validateStatus içine 304'ü açıkça ekleyin; aksi halde 304 için cache güncelleme kodunuz hiç çalışmayabilir.
import 'package:dio/dio.dart';
class CacheEntry {
CacheEntry(this.data, this.etag, this.expiresAt);
final Object? data;
final String? etag;
final DateTime expiresAt;
}
abstract class CacheStore {
Future<CacheEntry?> read(String key);
Future<void> write(String key, CacheEntry entry);
}
class EtagCacheInterceptor extends Interceptor {
EtagCacheInterceptor(this.store);
final CacheStore store;
String key(RequestOptions o) =>
'${o.extra['accountId']}|${o.method}|${o.uri}|${o.headers['Accept-Language']}';
@override
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (options.method != 'GET') return handler.next(options);
final entry = await store.read(key(options));
if (entry == null) return handler.next(options);
if (DateTime.now().isBefore(entry.expiresAt)) {
return handler.resolve(Response(
requestOptions: options,
statusCode: 200,
data: entry.data,
headers: Headers.fromMap({'x-app-cache': ['HIT']}),
));
}
options.extra['staleEntry'] = entry;
if (entry.etag != null) options.headers['If-None-Match'] = entry.etag;
handler.next(options);
}
@override
Future<void> onResponse(Response response, ResponseInterceptorHandler handler) async {
final stale = response.requestOptions.extra['staleEntry'] as CacheEntry?;
if (response.statusCode == 304 && stale != null) {
final refreshed = CacheEntry(stale.data, stale.etag,
DateTime.now().add(const Duration(minutes: 5)));
await store.write(key(response.requestOptions), refreshed);
return handler.resolve(Response(
requestOptions: response.requestOptions,
statusCode: 200,
data: stale.data,
headers: Headers.fromMap({'x-app-cache': ['REVALIDATED']}),
));
}
handler.next(response);
}
}
final dio = Dio(BaseOptions(
validateStatus: (status) => status != null &&
((status >= 200 && status < 300) || status == 304),
));Örnekteki sabit 5 dakikalık TTL'yi üretimde yanıt başlıklarından türetin. Ayrıca eşzamanlı beş ekran aynı bayat kaynağı isterse beş adet doğrulama isteği göndermeyin. Anahtar başına Map<String, Future<Response>> ile in-flight request coalescing uygulayın. Bu, özellikle sekme geçişlerinde aynı endpoint'e paralel GET atılmasını azaltır; ancak başarısız olan Future'yi whenComplete içinde map'ten silmezseniz sonraki çağrılar kalıcı olarak başarısız Future'ye bağlanır.
Disk cache şeması, Vary ve kullanıcı izolasyonu
Küçük metadata için Drift veya sqflite ile SQLite kullanın; büyük JSON gövdelerini doğrudan tek tabloda tutmak yerine dosya yolunu saklayın. Çünkü yüzlerce KB'lık gövdelerin sık güncellenmesi SQLite sayfa büyümesi ve vakum maliyeti oluşturabilir. Aşağıdaki şema, account kapsamını ve Vary tarafından etkilenebilen dil bilgisini ayrı alanlara taşır; bu alanlar bileşik benzersiz anahtarın parçasıdır.
CREATE TABLE http_cache (
cache_key TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
uri TEXT NOT NULL,
locale TEXT NOT NULL,
etag TEXT,
stored_at_ms INTEGER NOT NULL,
fresh_until_ms INTEGER NOT NULL,
stale_until_ms INTEGER NOT NULL,
body_path TEXT NOT NULL,
body_bytes INTEGER NOT NULL,
last_access_ms INTEGER NOT NULL
);
CREATE INDEX idx_http_cache_eviction
ON http_cache(last_access_ms ASC);Yazma sırası önemlidir: gövdeyi önce geçici dosyaya yazın, fsync gereksiniminize göre kapatın, dosyayı hedef adına atomik taşıyın, ardından SQLite transaction içinde metadata satırını güncelleyin. Ters sırada işlem yaparsanız uygulama yazma anında kapanınca veritabanında var görünen fakat dosyası olmayan bir kayıt oluşur. Uygulama açılışında body_path bulunamayan satırları temizleyen bir bakım sorgusu çalıştırın ve toplam body_bytes değeri örneğin 50 MB sınırını aşınca en eski last_access_ms kayıtlarını silin.
Bearer token'ı, oturum çerezini veya tam Authorization başlığını cache key içine koymayın: log, crash dump veya SQLite incelemesinde sızabilir. Bunun yerine oturum açıldığında üretilen uygulama içi hesap kapsamı kullanın ve logout sırasında DELETE FROM http_cache WHERE account_id = ? çalıştırın. Birden çok hesapla giriş yapılabilen cihazlarda bu silme işleminin atlanması, HTTP cache tasarımlarındaki en yaygın veri sızıntısı hatalarından biridir.
Flutter state management katmanında stale-while-revalidate akışı
flutter state management katmanına HTTP başlığı veya Dio Response taşımayın. Repository, UI için anlamlı bir kaynak durumu döndürsün: veri, cache kaynağı ve yenileme bayrağı. Riverpod kullanıyorsanız ekranda cache verisini korurken ağ yenilemesini görünür kılmak için tek bir AsyncValue<CatalogState> yerine kaynak bilgisini içeren immutable bir state kullanabilirsiniz.
class CatalogState {
const CatalogState({required this.items, required this.refreshing, required this.source});
final List<Product> items;
final bool refreshing;
final String source; // memory, disk, network, revalidated
CatalogState copyWith({bool? refreshing, String? source, List<Product>? items}) =>
CatalogState(
items: items ?? this.items,
refreshing: refreshing ?? this.refreshing,
source: source ?? this.source,
);
}
class CatalogNotifier extends AsyncNotifier<CatalogState> {
@override
Future<CatalogState> build() async {
final cached = await ref.read(catalogRepositoryProvider).readCache();
if (cached != null) {
_revalidate();
return CatalogState(items: cached, refreshing: true, source: 'disk');
}
return _fetchNetwork();
}
Future<void> _revalidate() async {
try {
final result = await ref.read(catalogRepositoryProvider).getConditional();
state = AsyncData(CatalogState(
items: result.items,
refreshing: false,
source: result.was304 ? 'revalidated' : 'network',
));
} catch (_) {
final current = state.valueOrNull;
if (current != null) state = AsyncData(current.copyWith(refreshing: false));
}
}
Future<CatalogState> _fetchNetwork() async => throw UnimplementedError();
}Buradaki edge case, arka plan yenilemesi hata verdiğinde cache verisini AsyncError ile ezmemektir. Kullanıcı son bilinen kataloğu görmeye devam etmeli, fakat ekran bir uyarı veya son güncellenme zamanını göstermelidir. Bu model, cross platform mobil uygulama geliştirme sürecinde Android ve iOS için aynı davranışı üretir; platforma özgü HTTP cache farklılıklarını widget katmanına sızdırmaz.
Cache etkisini Flutter DevTools ve ağ ölçümüyle doğrulamak
HTTP cache değişikliğini sadece ekranın hızlı hissettirmesiyle kabul etmeyin. Önce ve sonra aynı veri setinde üç senaryo ölçün: temiz kurulum, taze cache, bayat cache artı 304. Profil derlemesini flutter run --profile ile çalıştırın; Flutter DevTools Performance görünümünde ekran açılışı sırasında UI thread zaman çizelgesini kontrol edin. Ağ gecikmesini ise uygulama içinden Timeline event ve Dio ölçümüyle kaydedin; DevTools'taki kare süreleri tek başına HTTP yanıt süresini açıklamaz.
import 'dart:developer' as developer;
import 'package:dio/dio.dart';
class NetworkTimingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.extra['startedAt'] = Stopwatch()..start();
developer.Timeline.startSync('http:${options.method}', arguments: {
'uri': options.uri.toString(),
});
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final watch = response.requestOptions.extra['startedAt'] as Stopwatch;
watch.stop();
developer.Timeline.finishSync();
developer.log('http_ms=${watch.elapsedMilliseconds} cache='
'${response.headers.value('x-app-cache') ?? 'MISS'} '
'status=${response.statusCode}');
handler.next(response);
}
}Her senaryoyu en az 30 kez çalıştırıp median ve p95 hesaplayın. Örneğin rapora cache_hit_ratio = HIT / (HIT + MISS + REVALIDATED), p50 ekran-veri süresi ve indirilen toplam byte ekleyin. Yerel ağın değişkenliğini azaltmak için Android emülatör veya cihaz trafiğini mitmproxy üzerinden sabit gecikme profiline yönlendirebilir, ardından aynı istek dizisini tekrar oynatabilirsiniz. Beklenen karşılaştırma şudur: taze cache senaryosunda ağ byte sayısı sıfıra yaklaşmalı; 304 senaryosunda gövde transferi yerine yalnızca başlık trafiği görülmeli; UI thread'de JSON parse veya disk okuma yüzünden yeni uzun kareler oluşmamalıdır.
dart programlama eğitimi kapsamında özellikle dikkat edilmesi gereken ayrıntı, büyük JSON decode işleminin cache hit olsa bile ana isolate'ı meşgul edebilmesidir. 1 MB üzerindeki gövdelerde ham JSON metnini ölçün ve gerekiyorsa Isolate.run(() => jsonDecode(raw)) ile decode maliyetini ayırın. Ancak sonucu tekrar diskten okuyup yeniden encode etmeyin; bu işlem cache hit gecikmesini artırır. Ölçüm raporunda ağ kazancı ile decode maliyetini ayrı sütunlarda tutmak, yanlış optimizasyonu önler.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
flutter state management ile stale-while-revalidate nasıl uygulanır?
Repository önce diskten CatalogState verisini döndürmeli, ardından koşullu GET başlatmalıdır. Riverpod notifier yenileme sırasında mevcut AsyncData değerini AsyncError ile değiştirmemeli; refreshing=true durumunu koruyup yalnızca başarılı 200 veya 304 sonrasında state'i güncellemelidir.
cross platform mobil uygulama geliştirme projelerinde Dio HTTP cache yeterli mi?
Dio tek başına kalıcı ve HTTP semantiğine uyumlu bir cache sağlamaz. Cache-Control, ETag, Vary, kullanıcı kapsamı, disk tahliyesi ve 304 gövde birleştirmesini repository veya interceptor katmanında tanımlamanız gerekir. iOS ve Android'de aynı anahtar üretimini ve logout temizliğini ortak Dart kodunda tutun.
flutter eğitimi sırasında ETag 304 yanıtı neden hata olarak görünüyor?
Birçok HTTP istemcisi varsayılan olarak yalnızca 2xx durumlarını başarılı kabul eder. Dio BaseOptions içinde 304'ü validateStatus ile kabul etmezseniz yanıt onError akışına gider. 304 geldiğinde yeni gövde beklemeyin; saklanan gövdeyi kullanın ve yeni Cache-Control veya ETag başlıklarıyla metadata süresini güncelleyin.
dart programlama eğitimi için cache key içine Authorization yazmak güvenli mi?
Hayır. Ham Authorization değeri SQLite kaydında, debug logunda veya hata raporunda kalabilir. Bunun yerine accountId gibi token olmayan bir kullanıcı kapsamı, URI, HTTP metodu ve Vary tarafından belirtilen Accept-Language gibi başlıkları anahtarın bileşeni yapın. Logout'ta o accountId altındaki cache dosyalarını ve metadata satırlarını birlikte silin.
AI / LLM Discovery
Bu makale Opendart Akademi Flutter eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


