Last updated:
0 purchases
json cache
json_cache #
Contents #
Overview
Getting Started
Storing Simple Values
Suggested Dependency Relationship
Implementations
JsonCacheMem — Thread-safe In-memory cache
JsonCacheTry — Enhanced Diagnostic Messages
JsonCacheSharedPreferences — SharedPreferences
JsonCacheLocalStorage — LocalStorage
JsonCacheSafeLocalStorage — SafeLocalStorage
JsonCacheFlutterSecureStorage — FlutterSecureStorage
JsonCacheHive — Hive
Unit Test Tips
Mocking
Fake Implementations
Widget Testing
Example of Widget Test Code
SharedPreferences in Tests
Demo application
Contribute
References
Overview #
Cache is a hardware or software component that stores data so that future
requests for that data can be served faster; the data stored in a cache might
be the result of an earlier computation or a copy of data stored elsewhere.
— Cache_(computing) (2021, August 22). In Wikipedia, The Free Encyclopedia.
Retrieved 09:55, August 22,
2021
JsonCache is an object-oriented package for local caching of user data
in json. It can also be considered as a layer on top of Flutter's local storage
packages that aims to unify them with a stable and elegant interface —
JsonCache.
Why Json?
Because most of the local storage packages available for Flutter applications
use Json as the data format.
There is a one-to-one relationship between Dart's built-in type Map<String, dynamic> and Json, which makes encoding/decoding data in Json a trivial task.
Getting Started #
This package gives developers great flexibility by providing a set of classes
that can be selected and grouped in various combinations to meet specific cache
requirements.
JsonCache
is the core Dart interface of this package and represents the concept of cached
data. It is defined as:
/// Represents cached data in json format.
abstract interface class JsonCache {
/// Frees up storage space — deletes all keys and values.
Future<void> clear();
/// Removes cached data located at [key].
Future<void> remove(String key);
/// Retrieves cached data located at [key] or `null` if a cache miss occurs.
Future<Map<String, dynamic>?> value(String key);
/// It either updates data located at [key] with [value] or, if there is no
/// data at [key], creates a new cache row at [key] with [value].
///
/// **Note**: [value] must be json encodable.
Future<void> refresh(String key, Map<String, dynamic> value);
/// Checks for cached data located at [key].
///
/// Returns `true` if there is cached data at [key]; `false` otherwise.
Future<bool> contains(String key);
/// The cache keys.
///
/// Returns an **unmodifiable** list of all cache keys without duplicates.
Future<UnmodifiableListView<String>> keys();
}
copied to clipboard
It is reasonable to consider each cache entry (a key/data pair) as a group of
related data. Thus, it is expected to cache data into groups, where a key
represents the name of a single data group. For example:
'profile': {'name': 'John Doe', 'email': '[email protected]', 'accountType': 'premium'};
'preferences': {'theme': {'dark': true}, 'notifications': {'enabled': true}}
copied to clipboard
Above, the profile key is associated with profile-related data, while
the preferences key is associated with the user's preferences.
A typical code for saving the previous profile and preferences data is:
final JsonCache jsonCache = … retrieve one of the JsonCache implementations.
…
await jsonCache.refresh('profile', {'name': 'John Doe', 'email': '[email protected]', 'accountType': 'premium'});
await jsonCache.refresh('preferences', {'theme': {'dark': true}, 'notifications':{'enabled': true}});
copied to clipboard
Storing Simple Values #
In order to store a simple value such as a string, int, double, etc,
define it as a map key whose associated value is a boolean placeholder value
set to true. For example:
/// Storing a phrase.
jsonCache.refresh('info', {'This is very important information.': true});
// later on…
// This variable is a Map containing a single key.
final cachedInfo = await jsonCache.value('info');
// The key itself is the content of the stored information.
final info = cachedInfo?.keys.first;
print(info); // 'This is very important information.'
copied to clipboard
Suggested Dependency Relationship #
Whenever a function, method, or class needs to interact with cached user data,
this should be done via a reference to the JsonCache interface.
See the code snippet below:
/// Stores/retrieves user data from the device's local storage.
class JsonCacheRepository implements ILocalRepository {
/// Sets the [JsonCache] instance.
const JsonCacheRepository(this._cache);
// This class depends on an interface rather than any actual implementation
final JsonCache _cache;
/// Retrieves a cached email by [userId] or `null` if not found.
@override
Future<String?> getUserEmail(String userId) async {
final userData = await _cache.value(userId);
if (userData != null) {
// the email value or null if absent.
return userData['email'] as String?;
}
// There is no data associated with [userId].
return null;
}
}
copied to clipboard
By depending on an interface rather than an actual implementation, your code
becomes loosely coupled to this
package — which makes unit testing a lot easier.
Implementations #
The library
JsonCache
contains all classes that implement the
JsonCache
interface with more in-depth details.
The following sections are an overview of each implementation.
JsonCacheMem #
JsonCacheMem
is a thread-safe in-memory implementation of the JsonCache interface.
Moreover, it encapsulates a secondary cache or "slower level2 cache". Typically,
this secondary cache instance is responsible for the local cache; that is, it is
the JsonCache implementation that actually persists the data on the user's
device.
Typical Usage
Since JsonCacheMem is a
Decorator, you should
normally pass another JsonCache instance to it whenever you instantiate a
JsonCacheMem object. For example:
…
/// Cache initialization
final sharedPrefs = await SharedPreferences.getInstance();
final JsonCacheMem jsonCache = JsonCacheMem(JsonCacheSharedPreferences(sharedPrefs));
…
/// Saving profile and preferences data.
await jsonCache.refresh('profile', {'name': 'John Doe', 'email': '[email protected]', 'accountType': 'premium'});
await jsonCache.refresh('preferences', {'theme': {'dark': true}, 'notifications':{'enabled': true}});
…
/// Retrieving preferences data.
final Map<String, dynamic>? preferences = await jsonCache.value('preferences');
…
/// Frees up cached data before the user leaves the application.
Future<void> signout() async {
await jsonCache.clear();
}
…
/// Removes cached data related to a specific user.
Future<void> signoutId(String userId) async
await jsonCache.remove(userId);
}
copied to clipboard
Cache Initialization
JsonCacheMem.init
is the constructor whose purpose is to initialize the cache upon object
instantiation. The data passed to the init parameter is deeply copied to both
the internal in-memory cache and the level2 cache.
…
final LocalStorage storage = LocalStorage('my_data');
final Map<String, Map<String, dynamic>?> initData = await fetchData();
final JsonCacheMem jsonCache = JsonCacheMem.init(initData, level2:JsonCacheLocalStorage(storage));
…
copied to clipboard
JsonCacheTry #
JsonCacheTry
is an implementation of the JsonCache interface whose sole purpose is to
supply enhanced diagnostic information when a cache failure occurs. It does this
by throwing JsonCacheException
with the underlying stack trace.
Since JsonCacheTry is a
Decorator, you must pass
another JsonCache instance to it whenever you instantiate a JsonCacheTry
object. For example:
…
// Local storage cache initialization
final sharedPrefs = await SharedPreferences.getInstance();
// JsonCacheTry instance initialized with in-memory and local storage caches.
final jsonCacheTry = JsonCacheTry(JsonCacheMem(JsonCacheSharedPreferences(sharedPrefs)));
…
copied to clipboard
JsonCacheSharedPreferences #
JsonCacheSharedPreferences
is an implementation on top of the
shared_preferences package.
…
final sharedPrefs = await SharedPreferences.getInstance();
final JsonCache jsonCache = JsonCacheMem(JsonCacheSharedPreferences(sharedPrefs));
…
copied to clipboard
JsonCacheLocalStorage #
JsonCacheLocalStorage
is an implementation on top of the
localstorage package.
import 'package:flutter/material.dart';
import 'package:localstorage/localstorage.dart';
…
final LocalStorage storage = LocalStorage('my_data');
WidgetsFlutterBinding.ensureInitialized();
await initLocalStorage();
final JsonCache jsonCache = JsonCacheMem(JsonCacheLocalStorage(localStorage));
…
copied to clipboard
JsonCacheSafeLocalStorage #
JsonCacheSafeLocalStorage
is an implementation on top of the
safe_local_storage package.
…
final storage = SafeLocalStorage('/path/to/your/cache/file.json');
final JsonCache jsonCache = JsonCacheMem(JsonCacheSafeLocalStorage(storage));
…
copied to clipboard
JsonCacheFlutterSecureStorage #
JsonCacheFlutterSecureStorage
is an implementation on top of the
flutter_secure_storage package.
…
final flutterSecureStorage = FlutterSecureStorage(…);
final JsonCache jsonCache = JsonCacheFlutterSecureStorage(flutterSecureStorage);
// In order to write a string value, define it as a map key whose associated
// value is a boolean placeholder value set to 'true'.
jsonCache.refresh('secret', {'a secret info': true});
// later on…
final cachedInfo = await jsonCache.value('secret');
final info = cachedInfo?.keys.first; // 'a secret info'
copied to clipboard
JsonCacheHive #
JsonCacheHive
is an implementation on top of the hive
package.
…
await Hive.initFlutter(); // mandatory initialization.
final box = await Hive.openBox<String>('appBox'); // it must be a Box<String>.
final JsonCache hiveCache = JsonCacheMem(JsonCacheHive(box));
…
copied to clipboard
Unit Test Tips #
This package has been designed with unit testing in mind. This is one of the
reasons for the existence of the JsonCache interface.
Mocking #
Since JsonCache is the core interface of this package, you can easily
mock a implementation
that suits you when unit testing your code.
For example, with mocktail a mock
implementation should look like this:
import 'package:mocktail/mocktail.dart';
class JsonCacheMock extends Mock implements JsonCache {}
void main() {
// the mock instance.
final jsonCacheMock = JsonCacheMock();
test('should retrieve the preferences data', () async {
// Stub the 'value' method.
when(() => jsonCacheMock.value('preferences')).thenAnswer(
(_) async => <String, dynamic>{
'theme': {'dark': true},
'notifications': {'enabled': true}
},
);
// Verify no interactions have occurred.
verifyNever(() => jsonCacheMock.value('preferences'));
// Interact with the jsonCacheMock instance.
final preferencesData = await jsonCacheMock.value('preferences');
// Assert
expect(
preferencesData,
equals(
<String, dynamic>{
'theme': {'dark': true},
'notifications': {'enabled': true}
},
),
);
// Check if the interaction occurred only once.
verify(() => jsonCacheMock.value('preferences')).called(1);
});
}
copied to clipboard
Fake Implementations #
In addition to mocking, there is another approach to unit testing: making use of
a 'fake' implementation. Usually this so-called 'fake' implementation provides
the functionality required by the JsonCache interface without touching the
device's local storage. An example of this implementation is the
JsonCacheFake
class — whose sole purpose is to help developers with unit tests.
Widget Testing #
Because of the asynchronous nature of dealing with cached data, you're better
off putting all your test code inside a tester.runAsync method; otherwise,
your test case may stall due to a
deadlock caused by a race
condition
as there might be multiple Futures trying to access the same resources at the
same time.
Example of Widget Test Code
Your widget test code should look similar to the following code snippet:
testWidgets('refresh cached value', (WidgetTester tester) async {
final LocalStorage localStorage = LocalStorage('my_cached_data');
final jsonCache = JsonCacheMem(JsonCacheLocalStorage(localStorage));
tester.runAsync(() async {
// asynchronous code inside runAsync.
await jsonCache.refresh('test', <String, dynamic>{'aKey': 'aValue'});
});
});
copied to clipboard
SharedPreferences in Tests #
Whenever you run any unit tests involving the
shared_preferences package, you
must call the SharedPreferences.setMockInitialValues() function at the very
beginning of the test file; otherwise, the system may throw an error whose
description is: 'Binding has not yet been initialized'.
Example:
void main() {
SharedPreferences.setMockInitialValues({});
// the test cases come below
…
}
copied to clipboard
Demo application #
The demo application provides a fully working example, focused on demonstrating
the caching API in action. You can take the code in this demo and experiment
with it.
To run the demo application:
git clone https://github.com/dartoos-dev/json_cache.git
cd json_cache/example/
flutter run -d chrome
copied to clipboard
This should launch the demo application on Chrome in debug mode.
Contribute #
Contributors are welcome!
Open an issue regarding an improvement, a bug you noticed, or ask to be
assigned to an existing one.
If the issue is confirmed, fork the repository, do the changes on a
separate branch and make a Pull Request.
After review and acceptance, the PR is merged and closed.
Make sure the command below passes before making a Pull Request.
flutter analyze && flutter test
copied to clipboard
References #
Dart and race conditions
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.