dio_cache_interceptor

DartFlutterHTTP CacheInterceptorDioMobile DevelopmentPerformance

Cache Library

dio_cache_interceptor

Overview

dio_cache_interceptor is a cache interceptor library for the Dio HTTP client, designed for Flutter and Dart applications.

Details

dio_cache_interceptor is an interceptor library that provides comprehensive HTTP caching functionality for Dio, a popular HTTP client library in Dart. It supports multiple storage options with respect for HTTP directives (ETag, Last-Modified, Cache-Control) or custom cache policies. Available storage options include MemCacheStore (volatile cache with LRU strategy), HiveCacheStore (using hive_ce package), FileCacheStore (filesystem cache, excluding web), IsarCacheStore (Isar package supporting all platforms), and SembastCacheStore (Sembast package). It enables robust offline support through cache hits on network failures, cache usage on specific HTTP error codes, and maxStale settings for expired cache usage. The library plays a crucial role in improving network performance and user experience in Flutter applications by providing intelligent caching strategies.

Pros and Cons

Pros

  • Multiple Storage Support: Choose from memory, Hive, file, Isar, and Sembast storage
  • HTTP Directives Compliance: Support for standard cache headers like ETag and Cache-Control
  • Offline Support: Automatic cache fallback during network failures
  • Flutter Optimized: Performance improvements for mobile applications
  • Flexible Policies: Configurable custom cache policies
  • Error Handling: Cache usage on specific HTTP status codes
  • Cross-platform: Support for iOS, Android, Web, and desktop

Cons

  • Dio Dependency: Exclusive to Dio library, cannot be used with other HTTP clients
  • Configuration Complexity: Complex initial setup due to diverse options
  • Storage Limitations: FileCacheStore limitations in web environments
  • Memory Usage: Memory consumption by cache storage
  • Debug Difficulty: Challenges in cache behavior verification and debugging

Key Links

Usage Examples

Basic Setup

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';

void main() async {
  // Global cache options configuration
  final options = CacheOptions(
    store: MemCacheStore(),
    policy: CachePolicy.request,
    hitCacheOnErrorCodes: [401, 403, 500],
    hitCacheOnNetworkFailure: true,
    maxStale: const Duration(days: 7),
    priority: CachePriority.normal,
    cipher: null,
    keyBuilder: CacheOptions.defaultCacheKeyBuilder,
    allowPostMethod: false,
  );

  // Create Dio instance and add interceptor
  final dio = Dio();
  dio.interceptors.add(DioCacheInterceptor(options: options));

  // Execute API request
  try {
    final response = await dio.get('https://api.example.com/users');
    print('User data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

Multiple Storage Usage

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';

class APIClient {
  late Dio _dio;
  
  APIClient() {
    _dio = Dio();
    _setupCacheInterceptors();
  }
  
  void _setupCacheInterceptors() {
    // Short-term cache (memory)
    final memoryOptions = CacheOptions(
      store: MemCacheStore(maxSize: 10485760), // 10MB
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [500, 502, 503],
      maxStale: const Duration(hours: 1),
    );
    
    // Long-term cache (Hive)
    final persistentOptions = CacheOptions(
      store: HiveCacheStore('cache_box'),
      policy: CachePolicy.forceCache,
      hitCacheOnErrorCodes: [401, 403, 404, 500],
      maxStale: const Duration(days: 30),
    );
    
    _dio.interceptors.add(DioCacheInterceptor(options: memoryOptions));
  }
  
  // Get user profile (short-term cache)
  Future<Map<String, dynamic>> getUserProfile(int userId) async {
    final response = await _dio.get(
      '/users/$userId',
      options: Options(
        extra: {
          CacheKey.options: CacheOptions(
            policy: CachePolicy.refreshForceCache,
            maxStale: const Duration(minutes: 30),
          ),
        },
      ),
    );
    return response.data;
  }
  
  // Get app configuration (long-term cache)
  Future<Map<String, dynamic>> getAppConfig() async {
    final response = await _dio.get(
      '/config',
      options: Options(
        extra: {
          CacheKey.options: CacheOptions(
            store: HiveCacheStore('config_cache'),
            policy: CachePolicy.cacheFirst,
            maxStale: const Duration(days: 7),
          ),
        },
      ),
    );
    return response.data;
  }
}

Custom Cache Policy Implementation

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';

class SmartCacheClient {
  late Dio _dio;
  
  SmartCacheClient() {
    _dio = Dio();
    _setupSmartCaching();
  }
  
  void _setupSmartCaching() {
    final options = CacheOptions(
      store: MemCacheStore(),
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [500, 502, 503, 504],
      hitCacheOnNetworkFailure: true,
      maxStale: const Duration(hours: 24),
      keyBuilder: _customKeyBuilder,
    );
    
    _dio.interceptors.add(DioCacheInterceptor(options: options));
  }
  
  // Custom key builder for user-specific cache
  String _customKeyBuilder(RequestOptions request) {
    final userId = request.headers['User-ID'] ?? 'anonymous';
    final baseKey = CacheOptions.defaultCacheKeyBuilder(request);
    return '${userId}_$baseKey';
  }
  
  // Tiered cache strategy
  Future<T> fetchWithFallback<T>(
    String endpoint,
    T Function(dynamic) deserializer, {
    Duration? cacheMaxAge,
    bool forceRefresh = false,
  }) async {
    try {
      // 1. Try fresh data
      final freshResponse = await _dio.get(
        endpoint,
        options: Options(
          extra: {
            CacheKey.options: CacheOptions(
              policy: forceRefresh 
                ? CachePolicy.refresh 
                : CachePolicy.refreshForceCache,
              maxStale: cacheMaxAge ?? const Duration(hours: 1),
            ),
          },
        ),
      );
      
      return deserializer(freshResponse.data);
      
    } catch (e) {
      // 2. Use stale cache on network error
      final staleResponse = await _dio.get(
        endpoint,
        options: Options(
          extra: {
            CacheKey.options: CacheOptions(
              policy: CachePolicy.cacheFirst,
              maxStale: const Duration(days: 30), // Allow old cache
            ),
          },
        ),
      );
      
      return deserializer(staleResponse.data);
    }
  }
}

// Usage example
class User {
  final int id;
  final String name;
  final String email;
  
  User({required this.id, required this.name, required this.email});
  
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

void main() async {
  final client = SmartCacheClient();
  
  try {
    final user = await client.fetchWithFallback<User>(
      '/users/123',
      (data) => User.fromJson(data),
      cacheMaxAge: const Duration(minutes: 15),
    );
    
    print('User: ${user.name} (${user.email})');
  } catch (e) {
    print('Failed to fetch user: $e');
  }
}

Real-time Data and Cache Combination

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';

class NewsService {
  late Dio _dio;
  
  NewsService() {
    _dio = Dio();
    _setupNewsCache();
  }
  
  void _setupNewsCache() {
    final options = CacheOptions(
      store: HiveCacheStore('news_cache'),
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [500, 502, 503],
      hitCacheOnNetworkFailure: true,
      maxStale: const Duration(hours: 6),
    );
    
    _dio.interceptors.add(DioCacheInterceptor(options: options));
  }
  
  // Latest news (short-term cache)
  Future<List<Article>> getLatestNews() async {
    final response = await _dio.get(
      '/news/latest',
      options: Options(
        extra: {
          CacheKey.options: CacheOptions(
            policy: CachePolicy.refreshForceCache,
            maxStale: const Duration(minutes: 5), // Stale after 5 minutes
          ),
        },
      ),
    );
    
    return (response.data as List)
        .map((json) => Article.fromJson(json))
        .toList();
  }
  
  // Category news (medium-term cache)
  Future<List<Article>> getNewsByCategory(String category) async {
    final response = await _dio.get(
      '/news/category/$category',
      options: Options(
        extra: {
          CacheKey.options: CacheOptions(
            policy: CachePolicy.cacheFirst,
            maxStale: const Duration(hours: 1),
          ),
        },
      ),
    );
    
    return (response.data as List)
        .map((json) => Article.fromJson(json))
        .toList();
  }
  
  // Article details (long-term cache)
  Future<Article> getArticleDetail(int articleId) async {
    final response = await _dio.get(
      '/news/articles/$articleId',
      options: Options(
        extra: {
          CacheKey.options: CacheOptions(
            policy: CachePolicy.cacheFirst,
            maxStale: const Duration(days: 1),
          ),
        },
      ),
    );
    
    return Article.fromJson(response.data);
  }
  
  // Clear cache
  Future<void> clearCache() async {
    final store = HiveCacheStore('news_cache');
    await store.clean();
  }
}

class Article {
  final int id;
  final String title;
  final String content;
  final DateTime publishedAt;
  
  Article({
    required this.id,
    required this.title,
    required this.content,
    required this.publishedAt,
  });
  
  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'],
      title: json['title'],
      content: json['content'],
      publishedAt: DateTime.parse(json['published_at']),
    );
  }
}

Offline Support and Error Handling

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

class OfflineAwareAPIClient {
  late Dio _dio;
  final Connectivity _connectivity = Connectivity();
  
  OfflineAwareAPIClient() {
    _dio = Dio();
    _setupOfflineCache();
  }
  
  void _setupOfflineCache() {
    final options = CacheOptions(
      store: HiveCacheStore('offline_cache'),
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [400, 401, 403, 404, 500, 502, 503, 504],
      hitCacheOnNetworkFailure: true,
      maxStale: const Duration(days: 30), // Long-term offline cache
    );
    
    _dio.interceptors.add(DioCacheInterceptor(options: options));
    _dio.interceptors.add(_createOfflineInterceptor());
  }
  
  Interceptor _createOfflineInterceptor() {
    return InterceptorsWrapper(
      onRequest: (options, handler) async {
        final connectivityResult = await _connectivity.checkConnectivity();
        
        if (connectivityResult == ConnectivityResult.none) {
          // Force cache usage when offline
          options.extra[CacheKey.options] = CacheOptions(
            policy: CachePolicy.cacheStoreFirst,
            maxStale: const Duration(days: 365), // Allow very old cache
          );
        }
        
        handler.next(options);
      },
      onError: (error, handler) async {
        // Fallback on network errors
        if (error.type == DioErrorType.connectTimeout ||
            error.type == DioErrorType.receiveTimeout ||
            error.type == DioErrorType.sendTimeout) {
          
          print('Network error detected, falling back to cache');
          
          try {
            final cachedResponse = await _getCachedResponse(error.requestOptions);
            if (cachedResponse != null) {
              handler.resolve(cachedResponse);
              return;
            }
          } catch (e) {
            print('Failed to recover from cache: $e');
          }
        }
        
        handler.next(error);
      },
    );
  }
  
  Future<Response?> _getCachedResponse(RequestOptions options) async {
    final cacheOptions = CacheOptions(
      store: HiveCacheStore('offline_cache'),
      policy: CachePolicy.cacheStoreFirst,
      maxStale: const Duration(days: 365),
    );
    
    try {
      final dio = Dio();
      dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));
      
      return await dio.request(
        options.path,
        options: Options(
          method: options.method,
          headers: options.headers,
          extra: {CacheKey.options: cacheOptions},
        ),
        queryParameters: options.queryParameters,
        data: options.data,
      );
    } catch (e) {
      return null;
    }
  }
  
  Future<T> fetchWithOfflineSupport<T>(
    String endpoint,
    T Function(dynamic) deserializer,
  ) async {
    try {
      final response = await _dio.get(endpoint);
      return deserializer(response.data);
    } on DioError catch (e) {
      if (e.response?.statusCode == 304) {
        // Not Modified - cache is valid
        print('Data retrieved from cache');
        return deserializer(e.response?.data);
      }
      rethrow;
    }
  }
}

// Usage example
void main() async {
  final client = OfflineAwareAPIClient();
  
  try {
    final data = await client.fetchWithOfflineSupport<Map<String, dynamic>>(
      '/api/important-data',
      (data) => data as Map<String, dynamic>,
    );
    
    print('Data fetched successfully: $data');
  } catch (e) {
    print('Data fetch failed (offline?): $e');
  }
}

Performance Monitoring and Debugging

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';

class CacheDebugInterceptor extends Interceptor {
  int _cacheHits = 0;
  int _cacheMisses = 0;
  int _networkRequests = 0;
  
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    _networkRequests++;
    print('🌐 Network request: ${options.method} ${options.uri}');
    super.onRequest(options, handler);
  }
  
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final isCacheHit = response.extra[CacheKey.isCacheHit] == true;
    
    if (isCacheHit) {
      _cacheHits++;
      print('💾 Cache hit: ${response.requestOptions.uri}');
    } else {
      _cacheMisses++;
      print('📡 Network response: ${response.requestOptions.uri}');
    }
    
    print('📊 Stats - Hits: $_cacheHits, Misses: $_cacheMisses, Requests: $_networkRequests');
    super.onResponse(response, handler);
  }
  
  double get hitRate => 
      (_cacheHits + _cacheMisses) > 0 
          ? _cacheHits / (_cacheHits + _cacheMisses)
          : 0.0;
  
  void resetStats() {
    _cacheHits = 0;
    _cacheMisses = 0;
    _networkRequests = 0;
  }
}

class MonitoredAPIClient {
  late Dio _dio;
  final CacheDebugInterceptor _debugInterceptor = CacheDebugInterceptor();
  
  MonitoredAPIClient() {
    _dio = Dio();
    _setupMonitoredCache();
  }
  
  void _setupMonitoredCache() {
    final options = CacheOptions(
      store: MemCacheStore(),
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [500, 502, 503],
      maxStale: const Duration(hours: 1),
    );
    
    _dio.interceptors.add(DioCacheInterceptor(options: options));
    _dio.interceptors.add(_debugInterceptor);
  }
  
  Future<void> benchmark() async {
    print('🚀 Cache benchmark starting');
    
    // Call same API multiple times
    final endpoints = ['/users/1', '/users/2', '/users/1', '/users/3', '/users/1'];
    
    for (final endpoint in endpoints) {
      try {
        await _dio.get(endpoint);
        await Future.delayed(const Duration(milliseconds: 100));
      } catch (e) {
        print('❌ Error: $e');
      }
    }
    
    print('📈 Final statistics:');
    print('   Cache hit rate: ${(_debugInterceptor.hitRate * 100).toStringAsFixed(1)}%');
    print('   Total requests: ${_debugInterceptor._networkRequests}');
  }
}