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.
To use this plugin, add cached_resource
as a dependency in your pubspec.yaml file.
From the box there is only In-Memory storage shipped with the package.
Other storages should be added as new dependencies:
- resource_storage_hive - simple persistent storage based on hive with simple JSON decoder.
- resource_storage_secure - secure persistent storage based flutter_secure_storage with simple JSON decoder.
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());
}
There are a few ways to create a resource depending on used storage.
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),
);
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);
}
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(),
);
}
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(),
);
}
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);
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);
}
Any contribution is welcome!