AeroCache โ๏ธ
A high-performance HTTP caching library for Dart/Flutter with zstd compression, ETag/Last-Modified revalidation, and full Cache-Control directive support.
Features โจ๐
- โก๏ธ High Performance: Efficient caching with zstd compression for optimal storage
- ๐ท๏ธ ETag Support: Automatic cache revalidation using ETag headers
- ๐ Last-Modified Support: Fallback cache validation using Last-Modified headers
- ๐งฉ Vary Header Support: Intelligent cache key generation based on Vary header specifications
- ๐ก๏ธ Cache Control Directives: Support for no-cache, no-store, must-revalidate, max-age, max-stale, min-fresh, only-if-cached, stale-while-revalidate, and stale-if-error
- ๐ Background Revalidation: Stale-while-revalidate support for serving stale content while updating cache
- ๐ ๏ธ Error Resilience: Stale-if-error support for serving cached content during network failures
- ๐ Progress Tracking: Real-time download progress callbacks
- ๐งน Automatic Cleanup: Built-in expired cache cleanup
- โ๏ธ Flexible Configuration: Customizable cache directory and compression settings
- ๐จ Exception Handling: Comprehensive error handling with custom exceptions
Installation ๐ ๏ธ
Add this to your package's pubspec.yaml
file:
dependencies:
aero_cache: ^1.0.0
Then run:
flutter pub get
Usage ๐ฆ
Basic Usage ๐
import 'package:aero_cache/aero_cache.dart';
void main() async {
// Create AeroCache instance
final cache = AeroCache();
// Initialize the cache
await cache.initialize();
// Get data (downloads if not cached or stale)
final data = await cache.get('https://example.com/image.jpg');
// Use the data
print('Downloaded ${data.length} bytes');
// Clean up
cache.dispose();
}
Advanced Usage โ๏ธ
import 'package:aero_cache/aero_cache.dart';
void main() async {
// Create AeroCache with custom configuration
final cache = AeroCache(
disableCompression: false, // Enable zstd compression
compressionLevel: 6, // Custom compression level (1-22)
cacheDirPath: '/custom/cache/path', // Custom cache directory
defaultCacheDuration: const Duration(hours: 24), // Custom cache duration
);
await cache.initialize();
// Get data with progress tracking
final data = await cache.get(
'https://example.com/large-file.zip',
onProgress: (received, total) {
final progress = (received / total * 100).toStringAsFixed(1);
print('Download progress: $progress%');
},
);
// Get metadata information
final metaInfo = await cache.metaInfo('https://example.com/large-file.zip');
if (metaInfo != null) {
print('ETag: ${metaInfo.etag}');
print('Last Modified: ${metaInfo.lastModified}');
print('Is Stale: ${metaInfo.isStale}');
print('Expires At: ${metaInfo.expiresAt}');
print('Content Type: ${metaInfo.contentType}');
}
// Clear expired cache
await cache.clearExpiredCache();
// Clear all cache
await cache.clearAllCache();
cache.dispose();
}
Vary Header Handling ๐งฉ
import 'package:aero_cache/aero_cache.dart';
void main() async {
final cache = AeroCache();
await cache.initialize();
// First request with English accept-language
final data1 = await cache.get(
'https://api.example.com/content',
headers: {
'Accept-Language': 'en-US',
'User-Agent': 'MyApp/1.0',
'Accept-Encoding': 'gzip',
},
);
// Second request with different accept-language
// This will create a separate cache entry if the server's response
// includes "Vary: Accept-Language"
final data2 = await cache.get(
'https://api.example.com/content',
headers: {
'Accept-Language': 'ja-JP',
'User-Agent': 'MyApp/1.0',
'Accept-Encoding': 'gzip',
},
);
cache.dispose();
}
Cache Control Directives ๐ก๏ธ
import 'package:aero_cache/aero_cache.dart';
void main() async {
final cache = AeroCache();
await cache.initialize();
// Force bypass cache and download fresh data
final freshData = await cache.get(
'https://api.example.com/data',
noCache: true,
);
// Only use cached data, fail if not available
try {
final cachedData = await cache.get(
'https://api.example.com/data',
onlyIfCached: true,
);
} catch (e) {
print('No cached data available');
}
// Accept stale data up to 3600 seconds old
final staleData = await cache.get(
'https://api.example.com/data',
maxStale: 3600,
);
// Require data to be fresh for at least 300 seconds
final freshRequiredData = await cache.get(
'https://api.example.com/data',
minFresh: 300,
);
// Download without caching (no-store equivalent)
final temporaryData = await cache.get(
'https://api.example.com/data',
noStore: true,
);
cache.dispose();
}
Flutter Integration ๐ฆ
import 'package:flutter/material.dart';
import 'package:aero_cache/aero_cache.dart';
class ImageWidget extends StatefulWidget {
final String imageUrl;
const ImageWidget({Key? key, required this.imageUrl}) : super(key: key);
@override
_ImageWidgetState createState() => _ImageWidgetState();
}
class _ImageWidgetState extends State<ImageWidget> {
final AeroCache _cache = AeroCache();
Uint8List? _imageData;
bool _isLoading = true;
double _progress = 0.0;
@override
void initState() {
super.initState();
_loadImage();
}
Future<void> _loadImage() async {
try {
await _cache.initialize();
final data = await _cache.get(
widget.imageUrl,
onProgress: (received, total) {
setState(() {
_progress = received / total;
});
},
);
setState(() {
_imageData = data;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
// Handle error
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 8),
Text('${(_progress * 100).toInt()}%'),
],
);
}
if (_imageData != null) {
return Image.memory(_imageData!);
}
return const Icon(Icons.error);
}
@override
void dispose() {
_cache.dispose();
super.dispose();
}
}
Performance โก๏ธ
AeroCache uses zstd compression by default, which provides:
- Fast compression/decompression speeds
- Excellent compression ratios
- Low memory usage
Benchmarks show significant storage savings compared to uncompressed caching, especially for text-based content like JSON and HTML.
Vary Header Support ๐งฉ
AeroCache intelligently handles the Vary
header to ensure correct cache behavior when responses depend on request headers. When a server includes a Vary
header in its response, AeroCache automatically:
- Parses the
Vary
header to identify which request headers affect the response - Incorporates relevant request header values into the cache key calculation
- Ensures cache hits only occur when the specified request headers match exactly
- Supports common headers like
Accept-Encoding
,User-Agent
,Accept-Language
, etc.
This ensures that cached responses are served only when appropriate, preventing issues like serving compressed content to clients that don't support compression.
Contributing ๐ค
We welcome contributions to AeroCache! Please follow the GitHub Flow process:
How to Contribute
- Fork the repository on GitHub
- Create a feature branch from
main
:git checkout -b feature/your-feature-name
- Make your changes and add tests if applicable
- Ensure all tests pass:
flutter test
- Follow the code style using
very_good_analysis
:dart analyze
- Commit your changes with a clear message:
git commit -m "Add: your feature description"
- Push to your fork:
git push origin feature/your-feature-name
- Create a Pull Request on GitHub with:
- Clear description of changes
- Reference any related issues
- Screenshots if applicable
Development Setup ๐งโ๐ป
-
Clone the repository:
git clone https://github.com/your-username/aero_cache.git cd aero_cache
-
Install dependencies:
flutter pub get
-
Run tests:
flutter test
-
Run the example:
cd example flutter run
Code Style ๐จ
- Follow the official Dart style guide
- Use
very_good_analysis
linting rules - Write comprehensive tests for new features
- Add documentation for public APIs
Support ๐ฌ
For questions and support:
- Check the example directory for usage examples
- Open an issue on GitHub for bugs or feature requests
- Review the API documentation
Reporting Issues ๐
When reporting issues, please include:
- Flutter/Dart version
- Operating system
- Steps to reproduce
- Expected vs actual behavior
- Code samples if applicable
Changelog ๐
See CHANGELOG.md for version history and changes.
License ๐
This project is licensed under the MIT License - see the LICENSE file for details.