Skip to content

drstranges/cached_resource

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cached Resource

pub package

Cached resource implementation based on the NetworkBoundResource approach to follow the single source of truth principle.

Define a cached resource, subscribe for updates in multiple places, and be sure that a single network request will be called and all listeners receive a new value.

Usage

To use this plugin, add cached_resource as a dependency in your pubspec.yaml file.

Resource Storage

From the box there is only In-Memory storage shipped with the package.

Other storages should be added as new dependencies:

  1. resource_storage_hive - simple persistent storage based on hive with simple JSON decoder.
  2. resource_storage_secure - secure persistent storage based flutter_secure_storage with simple JSON decoder.

Configuration

In any place before usage of CachedResource, call ResourceConfig.setup and provide factories for persistent and/or secure storage.

Note: This step is required if you want to use CachedResource.persistent and CachedResource.secure. But it is optional if you will use CachedResource.inMemory or CachedResource.new.

void main() {
  // Configuration for cached_resource.
  ResourceConfig.setup(
    //inMemoryStorageFactory: const CustomMemoryResourceStorageProvider(),
    persistentStorageFactory: const HiveResourceStorageProvider(),
    secureStorageFactory: const FlutterSecureResourceStorageProvider(),
    logger: CustomLogger(),
  );

  runApp(const MyApp());
}

Define a resource/repository

There are a few ways to create a resource depending on used storage.

With In-Memory cache

See example.

class AccountBalanceRepository extends CachedResource<String, AccountBalance> {
  AccountBalanceRepository(AccountBalanceApi api)
      : super.inMemory(
          'account_balance',
          fetch: api.getAccountBalance,
          cacheDuration: const CacheDuration(minutes: 15),
        );
}

//or

final accountBalanceResource = CachedResource<String, AccountBalance>.inMemory(
  'account_balance',
  fetch: api.getAccountBalance,
  decode: AccountBalance.fromJson,
  cacheDuration: const CacheDuration(minutes: 15),
);

With persistent cache

The persistentStorageFactory should be already set by ResourceConfig.setup. See example.

class CategoryRepository {
  CategoryRepository(CategoryApi api)
      : _categoryResource = CachedResource.persistent(
          'categories',
          fetch: (_) => api.getCategories(),
          cacheDuration: const CacheDuration(days: 15),
          decode: Category.listFromJson,
          // Use executor only if [decode] callback does really heavy work,
          // for example if it parses a large json list with hundreds of heavy items
          executor: IsolatePoolExecutor().execute,
        );

  final CachedResource<String, List<Category>> _categoryResource;

  // We can use any constant key here, as the category list does not require any identifier.
  // But in some cases, you may need a unique key; for example, if you need to separate lists
  // by the currently authenticated user, you can use currentUserId as a key.
  final _key = 'key';

  Stream<Resource<List<Category>>> watchCategories() =>
      _categoryResource.asStream(_key);

  Future<void> removeCategoryFromCache(String categoryId) {
    return _categoryResource.updateCachedValue(
        _key,
            (categories) =>
            categorys?.where((category) => category.id != categoryId).toList());
  }

  Future<void> invalidate() => _categoryResource.invalidate(_key);
}

With secure cache

The secureStorageFactory should be already set by ResourceConfig.setup. See example.

class ProductSecretCodeRepository extends CachedResource<String, String> {
  ProductSecretCodeRepository(ProductApi api)
      : super.secure(
          'secret_code',
          fetch: api.getProductSecretCode,
          cacheDuration: const CacheDuration.newerStale(),
        );
}

With custom resource storage

You can create custom resource storage by extending ResourceStorage from resource_storage.

class UserRepository extends CachedResource<String, User> {
  UserRepository(UserApi api)
      : super(
          'users',
          fetch: api.getUserById,
          cacheDuration: const CacheDuration(days: 15),
          storage: YourCustomStorage(),
        );
}

Pageable resource

You can create a resource that can load a list of items page by page. See example.

class TransactionHistoryRepository
        extends OffsetPageableResource<String, TransactionItem> {
   TransactionHistoryRepository(TransactionHistoryApi api)
           : super.persistent(
      'transaction_history',
      loadPage: (filter, offset, limit) => api.getTransactionHistoryPage(filter, offset, limit),
      cacheDuration: const CacheDuration(minutes: 15),
      decode: TransactionItem.fromJson,
      pageSize: 15,
   );
//or
// : super.inMemory(
//     'transaction_history',
//     loadPage: api.getTransactionHistoryPage,
//     cacheDuration: const CacheDuration(minutes: 15),
//     pageSize: 15,
//   );
}

void init() {
// Then listen for a data
  transactionHistoryRepository.asStream(filter).listen(_showListOfItems);
}
// on need to load new page
void loadMore() => transactionHistoryRepository.loadNextPage(filter);

Listen for the resource stream or get a value

Call cachedResource.get(key) to get a single value. If the cache is not stale, it returns the cached value; otherwise, it triggers a new fetch request and returns the received value.

void foo() async {
  final resource = await resource.get(productId);
  if (resource.hasData) {
    final product = resource.data!;
    // do some work with product
  } else if (resource.isError) {
    final error = resource.error;
    final Product? productFromCache = resource.data;
    // show an error or use cached data
  }
}

To listen for resource updates, call cachedResource.asStream(key). It will emit Resource that can be one of 3 states:

  • Resoorce.loading(data) - fetch request triggered. Resource.data may contain old cached value.
  • Resoorce.success(data) - fetch request completed with fresh data or cache is not stale yet. Resource.data contains non null fresh value.
  • Resoorce.error(data, error) - fetch request completed with error. Resource.data may contain old cached value.
void startListening() async {
   _subscription = _categoryRepository.watchCategories().listen((resource) {
      if (resource.hasData) {
         final categories = resource.data!;
         // show categories
      } else if (resource.isError) {
         final error = resource.error!;
         final cachedCategories = resource.data;
         // handle error
      } else /*if (resource.isLoading)*/ { 
         // show loading state
      }
   });
}

// On need to reload categories from the server
void refresh() => _categoryRepository.invalidate();

void deleteCategory(String categoryId) async {
  await _api.deleteCategory(categoryId);
  // We don't want to reload the list from the server,
  // so we just delete the item from the list in cache.
  // Each observer will receive an updated list immediately.
  _categoryRepository.removeCategoryFromCache(categoryId);
}

Contribution

Any contribution is welcome!

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages