← Articles

Caching network requests in Flutter

By Charlin Joe · 25 August 2025

Network requests can be slow, especially on mobile. Caching reduces latency, saves battery, and makes your app usable offline. Here are the practical patterns for caching in Flutter.

What to cache and for how long

Product catalog, prices:     5-15 minutes (changes occasionally)
User profile:                1 hour (rarely changes)
Search results:              1-5 minutes (freshness matters)
Static content (T&Cs):      24 hours to forever
Auth token:                  Until expiry (usually 1 hour)
Realtime data (stock):       Don't cache (or < 30 seconds)

Dio cache interceptor

dependencies:
  dio: ^5.4.3
  dio_cache_interceptor: ^3.5.0
  dio_cache_interceptor_hive_store: ^3.2.0
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';

Future<Dio> buildCachedDio() async {
  final cacheDir = await getTemporaryDirectory();
  final store = HiveCacheStore(cacheDir.path);

  final options = CacheOptions(
    store: store,
    policy: CachePolicy.request, // Use cache if valid, else request
    hitCacheOnErrorExcept: [401, 403], // Use stale cache on errors (except auth errors)
    maxStale: const Duration(days: 7), // Max age for stale cache hits
    priority: CachePriority.normal,
  );

  return Dio()
    ..interceptors.add(DioCacheInterceptor(options: options));
}

Per-request cache control

Override the cache policy for specific requests:

// Force network, bypass cache
final response = await dio.get(
  '/orders',
  options: buildCacheOptions(const Duration(minutes: 5),
    policy: CachePolicy.refresh, // Always fetch from network, update cache
  ),
);

// Force cache only (offline mode)
final cached = await dio.get(
  '/products',
  options: buildCacheOptions(const Duration(hours: 1),
    policy: CachePolicy.forceCache,
  ),
);

// No cache for this request
final fresh = await dio.get(
  '/account/balance',
  options: buildCacheOptions(Duration.zero,
    policy: CachePolicy.noCache,
  ),
);

Manual Hive cache

For more control, cache responses manually:

class ProductRepository {
  final _dio = Dio();
  final _cache = Hive.box<String>('product_cache');
  static const _ttl = Duration(minutes: 10);

  Future<List<Product>> getProducts() async {
    // Check cache
    final cacheKey = 'products';
    final timestampKey = '${cacheKey}_timestamp';
    final cached = _cache.get(cacheKey);
    final timestamp = _cache.get(timestampKey);

    if (cached != null && timestamp != null) {
      final cacheTime = DateTime.parse(timestamp);
      if (DateTime.now().difference(cacheTime) < _ttl) {
        return (jsonDecode(cached) as List)
            .map((e) => Product.fromJson(e as Map<String, dynamic>))
            .toList();
      }
    }

    // Cache miss or expired — fetch from network
    final response = await _dio.get('/products');
    final products = (response.data as List)
        .map((e) => Product.fromJson(e as Map<String, dynamic>))
        .toList();

    // Update cache
    await _cache.put(cacheKey, jsonEncode(response.data));
    await _cache.put(timestampKey, DateTime.now().toIso8601String());

    return products;
  }

  Future<void> invalidateProducts() async {
    await _cache.delete('products');
    await _cache.delete('products_timestamp');
  }
}

Stale-while-revalidate

Show cached data immediately, then update from network:

Stream<List<Product>> getProductsStream() async* {
  // Yield cached data immediately
  final cached = _getCachedProducts();
  if (cached != null) yield cached;

  // Fetch fresh data
  try {
    final fresh = await _fetchFromNetwork();
    _updateCache(fresh);
    yield fresh;
  } catch (e) {
    // Network failed — cached data already yielded
    if (cached == null) rethrow; // Nothing to show
  }
}

// In widget:
StreamBuilder<List<Product>>(
  stream: repo.getProductsStream(),
  builder: (context, snapshot) {
    // Shows cached data first, then fresh data when available
    if (!snapshot.hasData) return const CircularProgressIndicator();
    return ProductList(products: snapshot.data!);
  },
)

Image caching

// cached_network_image handles image caching automatically
CachedNetworkImage(
  imageUrl: product.imageUrl,
  // Limit memory footprint
  memCacheWidth: 300,
  memCacheHeight: 300,
  // Control disk cache
  cacheManager: CacheManager(
    Config(
      'products_images',
      stalePeriod: const Duration(days: 7),
      maxNrOfCacheObjects: 200,
    ),
  ),
)

Riverpod with cache

@riverpod
class ProductsNotifier extends _$ProductsNotifier {
  @override
  Future<List<Product>> build() async {
    // Keep alive so cache persists across navigation
    ref.keepAlive();
    return ref.read(productRepositoryProvider).getProducts();
  }

  Future<void> refresh() async {
    await ref.read(productRepositoryProvider).invalidateProducts();
    ref.invalidateSelf();
  }
}

Common pitfalls

Caching POST responses. POST requests should never be cached — they change server state. Only cache GET requests.

Infinite TTL for user data. Caching user profile for 24 hours means a user who updates their name won't see the change for a day. Use short TTLs for user data or invalidate explicitly after updates.

Not invalidating cache after mutations. When a user places an order, the orders list cache should be invalidated so the next fetch shows the new order.

Sign in to like, dislike, or report.

Caching network requests in Flutter — ANN Tech