dio_cache_interceptor
キャッシュライブラリ
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制限
- メモリ使用量: キャッシュストレージによるメモリ消費
- デバッグ難易度: キャッシュの動作確認とデバッグの困難さ
主要リンク
- pub.dev - dio_cache_interceptor
- GitHub - dio_cache_interceptor
- Dio Documentation
- Flutter HTTP Caching Guide
書き方の例
基本的なセットアップ
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}');
}
}