remote_caching 1.0.12 copy "remote_caching: ^1.0.12" to clipboard
remote_caching: ^1.0.12 copied to clipboard

A Flutter package for caching remote API calls with configurable duration.

Remote Caching logo

๐Ÿ” Remote Caching ๐Ÿ“ฆ

A lightweight, flexible, and persistent cache layer for remote API calls in Flutter.
Avoid redundant network calls. Boost performance. Cache smartly.

Pub Points Pub Version GitHub Stars Very Good Analysis


A lightweight yet powerful Flutter package for caching asynchronous remote calls locally using SQLite โ€” with full support for expiration, serialization, and custom deserializers.

๐Ÿง  Save your API responses. ๐Ÿ” Avoid unnecessary calls. โšก Go fast. ๐Ÿ’ก Stay clean.


๐Ÿ“‹ Table of Contents #


โœจ Features #

  • โœ… Automatic caching of remote data with intelligent expiration
  • โณ Flexible expiration - use duration or exact datetime
  • ๐Ÿ”„ Manual cache invalidation (by key or clear all)
  • ๐Ÿ’พ SQLite-powered persistent cache with automatic cleanup
  • ๐Ÿงฉ Generic support for any data type (Map, List, custom models...)
  • ๐Ÿงฐ Custom deserialization with fromJson functions
  • ๐Ÿ“Š Cache statistics and monitoring
  • ๐Ÿงช Test-friendly with verbose logging and in-memory database support
  • ๐Ÿ›ก๏ธ Error handling - graceful fallback to remote calls
  • ๐Ÿ”ง Cross-platform support (iOS, Android, Web, Desktop)

๐ŸŽฏ Why RemoteCaching? #

  • ๐Ÿ” You need structured, persistent caching for remote API calls
  • ๐Ÿ’ก You want fine-grained control over serialization and expiration
  • ๐Ÿงผ You don't want to reinvent the wheel each time you need cache logic
  • โšก You want to reduce API calls and improve app performance
  • ๐Ÿ›ก๏ธ You need reliable error handling that won't break your app

๐Ÿš€ Quick Start #

1. Add the dependency #

flutter pub add remote_caching

2. Initialize the cache #

import 'package:remote_caching/remote_caching.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  await RemoteCaching.instance.init(
    defaultCacheDuration: Duration(hours: 1),
    verboseMode: true, // See logs in debug mode
  );
  
  runApp(MyApp());
}

3. Cache your first API call #

class UserService {
  Future<User> getUserProfile(String userId) async {
    return await RemoteCaching.instance.call<User>(
      'user_$userId',
      cacheDuration: Duration(minutes: 30),
      remote: () async => await fetchUserFromAPI(userId),
      fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
    );
  }
}

๐Ÿ› ๏ธ Usage Guide #

๐Ÿ“‹ Initialization #

Initialize the cache system with your preferred settings:

await RemoteCaching.instance.init(
  defaultCacheDuration: Duration(hours: 1), // Default cache duration
  verboseMode: true, // Enable detailed logging (default: kDebugMode)
  databasePath: '/custom/path/cache.db', // Custom database path (optional)
);

Parameters:

  • defaultCacheDuration: Default expiration time for cached items
  • verboseMode: Enable detailed logging for debugging
  • databasePath: Custom database path (uses default if not specified)

๐Ÿ”„ Basic Caching #

Cache a simple API call with automatic expiration:

final user = await RemoteCaching.instance.call<User>(
  'user_profile_123',
  cacheDuration: Duration(minutes: 30),
  remote: () async => await apiService.getUser(123),
  fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
);

โฐ Exact Expiration Time #

Use a specific expiration datetime instead of duration:

final user = await RemoteCaching.instance.call<User>(
  'user_profile_123',
  cacheExpiring: DateTime.now().add(Duration(hours: 2)),
  remote: () async => await apiService.getUser(123),
  fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
);

๐Ÿ“ Caching Lists and Complex Data #

Cache lists, maps, or any serializable data:

// Cache a list of users
final users = await RemoteCaching.instance.call<List<User>>(
  'all_users',
  remote: () async => await apiService.getAllUsers(),
  fromJson: (json) => (json as List)
      .map((item) => User.fromJson(item as Map<String, dynamic>))
      .toList(),
);

// Cache a map of settings
final settings = await RemoteCaching.instance.call<Map<String, dynamic>>(
  'app_settings',
  remote: () async => await apiService.getSettings(),
  fromJson: (json) => Map<String, dynamic>.from(json as Map),
);

๐Ÿ”„ Force Refresh #

Bypass cache and fetch fresh data:

final user = await RemoteCaching.instance.call<User>(
  'user_profile_123',
  forceRefresh: true, // Ignore cache, fetch from remote
  remote: () async => await apiService.getUser(123),
  fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
);

๐Ÿงน Cache Management #

Clear cache entries as needed:

// Clear specific cache entry
await RemoteCaching.instance.clearCacheForKey('user_profile_123');

// Clear all cache
await RemoteCaching.instance.clearCache();

๐Ÿ“Š Cache Statistics #

Monitor your cache usage:

final stats = await RemoteCaching.instance.getCacheStats();
print('Total entries: ${stats.totalEntries}');
print('Total size: ${stats.totalSizeBytes} bytes');
print('Expired entries: ${stats.expiredEntries}');

๐Ÿ’ก Advanced Usage #

๐Ÿง  In-Memory Database #

Use in-memory database for testing or temporary caching:

import 'package:remote_caching/src/common/get_in_memory_database.dart';

await RemoteCaching.instance.init(
  databasePath: getInMemoryDatabasePath(),
  verboseMode: true,
);

โš ๏ธ Warning: In-memory cache is lost on app restart. Avoid storing large datasets.

๐Ÿ”‘ Dynamic Cache Keys #

Generate cache keys dynamically based on parameters:

class ProductService {
  Future<Product> getProduct(String category, String id) async {
    final cacheKey = 'product_${category}_$id';
    
    return await RemoteCaching.instance.call<Product>(
      cacheKey,
      remote: () async => await apiService.getProduct(category, id),
      fromJson: (json) => Product.fromJson(json as Map<String, dynamic>),
    );
  }
}

๐Ÿ›ก๏ธ Error Handling #

The package handles serialization errors gracefully:

// If serialization fails, the remote call is used instead
// No app crashes, just logged errors
final data = await RemoteCaching.instance.call<ComplexModel>(
  'complex_data',
  remote: () async => await fetchComplexData(),
  fromJson: (json) => ComplexModel.fromJson(json as Map<String, dynamic>),
);

๐Ÿ”„ Cache Invalidation Strategies #

Implement different cache invalidation patterns:

class CacheManager {
  // Invalidate related cache entries
  Future<void> invalidateUserCache(String userId) async {
    await RemoteCaching.instance.clearCacheForKey('user_$userId');
    await RemoteCaching.instance.clearCacheForKey('user_profile_$userId');
    await RemoteCaching.instance.clearCacheForKey('user_settings_$userId');
  }
  
  // Invalidate all cache when user logs out
  Future<void> onUserLogout() async {
    await RemoteCaching.instance.clearCache();
  }
}

๐Ÿ“ฆ Complete Example #

Here's a complete example showing how to cache API responses in a Flutter app:

import 'package:flutter/material.dart';
import 'package:remote_caching/remote_caching.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class UserProfilePage extends StatefulWidget {
  @override
  _UserProfilePageState createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  User? _user;
  bool _isLoading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadUserProfile();
  }

  Future<void> _loadUserProfile() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final user = await RemoteCaching.instance.call<User>(
        'user_profile_123',
        cacheDuration: Duration(minutes: 30),
        remote: () async {
          final response = await http.get(
            Uri.parse('https://api.example.com/users/123'),
          );
          
          if (response.statusCode == 200) {
            return jsonDecode(response.body);
          } else {
            throw Exception('Failed to load user profile');
          }
        },
        fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
      );

      setState(() {
        _user = user;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  Future<void> _refreshProfile() async {
    // Force refresh from remote
    final user = await RemoteCaching.instance.call<User>(
      'user_profile_123',
      forceRefresh: true,
      remote: () async {
        final response = await http.get(
          Uri.parse('https://api.example.com/users/123'),
        );
        return jsonDecode(response.body);
      },
      fromJson: (json) => User.fromJson(json as Map<String, dynamic>),
    );

    setState(() {
      _user = user;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('User Profile'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _refreshProfile,
          ),
        ],
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return Center(child: CircularProgressIndicator());
    }
    
    if (_error != null) {
      return Center(child: Text('Error: $_error'));
    }
    
    if (_user == null) {
      return Center(child: Text('No user data'));
    }
    
    return Padding(
      padding: EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Name: ${_user!.name}', style: Theme.of(context).textTheme.headline6),
          Text('Email: ${_user!.email}'),
          Text('Age: ${_user!.age}'),
        ],
      ),
    );
  }
}

class User {
  final String name;
  final String email;
  final int age;

  User({required this.name, required this.email, required this.age});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'] as String,
      email: json['email'] as String,
      age: json['age'] as int,
    );
  }
}

A full working example is available in the example/ directory.


๐Ÿ“š API Reference #

RemoteCaching Class #

The main class for managing remote caching operations.

Methods

Method Description Parameters
init() Initialize the cache system defaultCacheDuration, verboseMode, databasePath
call<T>() Cache a remote call key, remote, fromJson, cacheDuration, cacheExpiring, forceRefresh
clearCache() Clear all cache entries None
clearCacheForKey() Clear specific cache entry key
getCacheStats() Get cache statistics None
dispose() Clean up resources None

Parameters Details

init() parameters:

  • defaultCacheDuration (Duration?): Default expiration time for cached items
  • verboseMode (bool): Enable detailed logging (default: kDebugMode)
  • databasePath (String?): Custom database path

call<T>() parameters:

  • key (String): Unique identifier for the cache entry
  • remote (Future
  • fromJson (T Function(Object? json)): Function to deserialize JSON data
  • cacheDuration (Duration?): How long to cache the data
  • cacheExpiring (DateTime?): Exact expiration datetime
  • forceRefresh (bool): Bypass cache and fetch fresh data

CachingStats Class #

Statistics about the current cache state.

class CachingStats {
  final int totalEntries;      // Total number of cached entries
  final int totalSizeBytes;    // Total size of cached data in bytes
  final int expiredEntries;    // Number of expired entries
}

โ“ FAQ #

Q: What happens if serialization or deserialization fails?
A: The error is logged, the cache is ignored, and the remote call is used. Your app will never crash due to cache errors.

Q: Can I use my own model classes?
A: Yes! Just provide a fromJson function and ensure your model supports toJson when caching. The package relies on jsonEncode / jsonDecode under the hood.

Q: Does it work offline?
A: Cached data is available offline until it expires or is cleared.

Q: Does it work on all platforms?
A: We use sqlite3 with sqflite_common_ffi to support all platforms. Refer to the packages docs for more information.

Q: Can I use a custom database path?
A: Yes! You can specify a custom database path using the databasePath parameter in the init() method.

Q: How do I handle cache invalidation?
A: Use clearCacheForKey() for specific entries or clearCache() for all entries. You can also use forceRefresh: true to bypass cache for a single call.

Q: What's the difference between cacheDuration and cacheExpiring?
A: cacheDuration sets expiration relative to now (e.g., 30 minutes from now), while cacheExpiring sets an absolute expiration datetime.

Q: Can I cache different types of data?
A: Yes! You can cache any serializable data: primitives, maps, lists, custom objects, etc. Just provide the appropriate fromJson function.

Q: Is the cache persistent?
A: Yes, by default the cache is stored in SQLite and persists between app launches. Use getInMemoryDatabasePath() for temporary in-memory caching.

Q: How do I monitor cache performance?
A: Use getCacheStats() to get statistics about cache usage, or enable verboseMode to see detailed logs.


๐Ÿค Contributing #

Contributions, issues and feature requests are welcome! Feel free to check issues page or submit a pull request.

Code Style #

This project follows the very_good_analysis linting rules.


Made with โค๏ธ by Eliatolin

23
likes
160
points
470
downloads
screenshot

Publisher

verified publishereliatolin.it

Weekly Downloads

A Flutter package for caching remote API calls with configurable duration.

Repository (GitHub)
View/report issues

Topics

#network #http #caching #remote

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, path, sqflite, sqflite_common_ffi

More

Packages that depend on remote_caching