AeroCache โ˜๏ธ

CI pub package style: very good analysis

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();
  }
}

API Reference ๐Ÿ“š

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

  1. Fork the repository on GitHub
  2. Create a feature branch from main:
    git checkout -b feature/your-feature-name
    
  3. Make your changes and add tests if applicable
  4. Ensure all tests pass:
    flutter test
    
  5. Follow the code style using very_good_analysis:
    dart analyze
    
  6. Commit your changes with a clear message:
    git commit -m "Add: your feature description"
    
  7. Push to your fork:
    git push origin feature/your-feature-name
    
  8. Create a Pull Request on GitHub with:
    • Clear description of changes
    • Reference any related issues
    • Screenshots if applicable

Development Setup ๐Ÿง‘โ€๐Ÿ’ป

  1. Clone the repository:

    git clone https://github.com/your-username/aero_cache.git
    cd aero_cache
    
  2. Install dependencies:

    flutter pub get
    
  3. Run tests:

    flutter test
    
  4. 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.

Libraries

aero_cache
src/exceptions
src/meta_info