dio_cache_interceptor

DartFlutterHTTPキャッシュインターセプターDioモバイル開発パフォーマンス

キャッシュライブラリ

dio_cache_interceptor

概要

dio_cache_interceptorは、Flutter・Dart向けのDio HTTPクライアントに対応したキャッシュインターセプターライブラリです。

詳細

dio_cache_interceptor(ダイオ・キャッシュ・インターセプター)は、Dartの人気HTTPクライアントライブラリであるDioに対して、包括的なHTTPキャッシュ機能を提供するインターセプターライブラリです。HTTPディレクティブ(ETag、Last-Modified、Cache-Control)を尊重した、または独自のキャッシュポリシーによる、複数のストレージオプションをサポートしています。MemCacheStore(LRU戦略のボラタイルキャッシュ)、HiveCacheStore(hive_ceパッケージを使用)、FileCacheStore(ファイルシステムキャッシュ、Web除く)、IsarCacheStore(全プラットフォーム対応のIsarパッケージ)、SembastCacheStore(Sembastパッケージ)など、用途に応じたストレージを選択できます。ネットワーク障害時のキャッシュヒット、特定HTTPエラーコードでのキャッシュ使用、maxStale設定による期限切れキャッシュの使用など、堅牢なオフライン対応が可能です。Flutterアプリケーションのネットワークパフォーマンス向上とユーザー体験の改善に重要な役割を果たしています。

メリット・デメリット

メリット

  • 複数ストレージ対応: メモリ、Hive、ファイル、Isar、Sembastから選択可能
  • HTTPディレクティブ準拠: ETag、Cache-Control等の標準的なキャッシュヘッダー対応
  • オフライン対応: ネットワーク障害時の自動キャッシュフォールバック
  • Flutter最適化: モバイルアプリケーションのパフォーマンス向上
  • 柔軟なポリシー: カスタムキャッシュポリシーの設定可能
  • エラーハンドリング: 特定HTTPステータスコードでのキャッシュ使用
  • クロスプラットフォーム: iOS、Android、Web、デスクトップ対応

デメリット

  • Dio依存: Dioライブラリ専用で他のHTTPクライアントでは使用不可
  • 設定複雑性: 多様なオプションによる初期設定の複雑さ
  • ストレージ制限: Web環境でのFileCacheStore制限
  • メモリ使用量: キャッシュストレージによるメモリ消費
  • デバッグ難易度: キャッシュの動作確認とデバッグの困難さ

主要リンク

書き方の例

基本的なセットアップ

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

void main() async {
  // グローバルキャッシュオプションの設定
  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,
  );

  // Dioインスタンスの作成とインターセプター追加
  final dio = Dio();
  dio.interceptors.add(DioCacheInterceptor(options: options));

  // APIリクエストの実行
  try {
    final response = await dio.get('https://api.example.com/users');
    print('ユーザーデータ: ${response.data}');
  } catch (e) {
    print('エラー: $e');
  }
}

複数ストレージの使い分け

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() {
    // 短期間キャッシュ(メモリ)
    final memoryOptions = CacheOptions(
      store: MemCacheStore(maxSize: 10485760), // 10MB
      policy: CachePolicy.request,
      hitCacheOnErrorCodes: [500, 502, 503],
      maxStale: const Duration(hours: 1),
    );
    
    // 長期間キャッシュ(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));
  }
  
  // ユーザー情報取得(短期キャッシュ)
  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;
  }
  
  // 設定情報取得(長期キャッシュ)
  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;
  }
}

カスタムキャッシュポリシーの実装

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));
  }
  
  // カスタムキーBuilderでユーザー固有のキャッシュ
  String _customKeyBuilder(RequestOptions request) {
    final userId = request.headers['User-ID'] ?? 'anonymous';
    final baseKey = CacheOptions.defaultCacheKeyBuilder(request);
    return '${userId}_$baseKey';
  }
  
  // 段階的キャッシュ戦略
  Future<T> fetchWithFallback<T>(
    String endpoint,
    T Function(dynamic) deserializer, {
    Duration? cacheMaxAge,
    bool forceRefresh = false,
  }) async {
    try {
      // 1. 新鮮なデータを試行
      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. ネットワークエラー時は期限切れキャッシュも使用
      final staleResponse = await _dio.get(
        endpoint,
        options: Options(
          extra: {
            CacheKey.options: CacheOptions(
              policy: CachePolicy.cacheFirst,
              maxStale: const Duration(days: 30), // 古いキャッシュも許可
            ),
          },
        ),
      );
      
      return deserializer(staleResponse.data);
    }
  }
}

// 使用例
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.name} (${user.email})');
  } catch (e) {
    print('ユーザー取得失敗: $e');
  }
}

リアルタイムデータとキャッシュの組み合わせ

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));
  }
  
  // 最新ニュース(短時間キャッシュ)
  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), // 5分で古い
          ),
        },
      ),
    );
    
    return (response.data as List)
        .map((json) => Article.fromJson(json))
        .toList();
  }
  
  // カテゴリ別ニュース(中時間キャッシュ)
  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();
  }
  
  // 詳細記事(長時間キャッシュ)
  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);
  }
  
  // キャッシュクリア
  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']),
    );
  }
}

オフライン対応とエラーハンドリング

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), // 長期間のオフラインキャッシュ
    );
    
    _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) {
          // オフライン時は強制的にキャッシュを使用
          options.extra[CacheKey.options] = CacheOptions(
            policy: CachePolicy.cacheStoreFirst,
            maxStale: const Duration(days: 365), // 非常に古いキャッシュも許可
          );
        }
        
        handler.next(options);
      },
      onError: (error, handler) async {
        // ネットワークエラー時のフォールバック
        if (error.type == DioErrorType.connectTimeout ||
            error.type == DioErrorType.receiveTimeout ||
            error.type == DioErrorType.sendTimeout) {
          
          print('ネットワークエラー検出、キャッシュにフォールバック');
          
          try {
            final cachedResponse = await _getCachedResponse(error.requestOptions);
            if (cachedResponse != null) {
              handler.resolve(cachedResponse);
              return;
            }
          } catch (e) {
            print('キャッシュからの復旧に失敗: $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 - キャッシュが有効
        print('キャッシュからデータを取得');
        return deserializer(e.response?.data);
      }
      rethrow;
    }
  }
}

// 使用例
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');
  } catch (e) {
    print('データ取得失敗(オフライン?): $e');
  }
}

パフォーマンス監視とデバッグ

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('🌐 ネットワークリクエスト: ${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('💾 キャッシュヒット: ${response.requestOptions.uri}');
    } else {
      _cacheMisses++;
      print('📡 ネットワークレスポンス: ${response.requestOptions.uri}');
    }
    
    print('📊 統計 - ヒット: $_cacheHits, ミス: $_cacheMisses, リクエスト: $_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('🚀 キャッシュベンチマーク開始');
    
    // 同じAPIを複数回呼び出し
    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('❌ エラー: $e');
      }
    }
    
    print('📈 最終統計:');
    print('   キャッシュヒット率: ${(_debugInterceptor.hitRate * 100).toStringAsFixed(1)}%');
    print('   総リクエスト数: ${_debugInterceptor._networkRequests}');
  }
}