json_cache 1.2.3 json_cache: ^1.2.3 copied to clipboard
An object-oriented package for caching user data locally in json; a combinable layer on top of local storage packages that unifies them with an elegant caching API.
json_cache #
Contents #
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.
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 interface of this package and represents the concept of cached data. It is defined as:
/// Represents cached data in json format.
abstract class JsonCache {
/// Frees up storage space — deletes all keys with associated 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);
}
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': 'johndoe@email.com', 'accountType': 'premium'};
'preferences': {'theme': {'dark': true}, 'notifications':{'enabled': true}}
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': 'johndoe@email.com', 'accountType': 'premium'});
await jsonCache.refresh('preferences', {'theme': {'dark': true}, 'notifications':{'enabled': true}});
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 is a thread-safe in-memory implementation of the JsonCache
interface.
Moreover, it encapsulates a secondary cache or "slower level2 cache". Typically,
the secondary cache instance is responsible for the local cache; that is, it is
the cache instance that persists data on the user's device.
Typical Usage
Due to the fact that JsonCacheMem
is a decorator, you should normally pass
another JsonCache
instance to it whenever you instantiate a JsonCacheMem
object. For example:
…
/// Cache initialization
final prefs = await SharedPreferences.getInstance();
final JsonCacheMem jsonCache = JsonCacheMem(JsonCachePrefs(prefs));
…
/// Saving profile and preferences data.
await jsonCache.refresh('profile', {'name': 'John Doe', 'email': 'johndoe@email.com', '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);
}
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 fetchInfo();
final JsonCacheMem jsonCache = JsonCacheMem.init(initData, level2:JsonCacheLocalStorage(storage));
…
JsonCachePrefs #
JsonCachePrefs is an implementation on top of the shared_preferences package.
…
final prefs = await SharedPreferences.getInstance();
final JsonCache jsonCache = JsonCacheMem(JsonCachePrefs(prefs));
…
JsonCacheEncPrefs #
JsonCacheEncPrefs is an implementation on top of the encrypted_shared_preferences package.
…
final encPrefs = EncryptedSharedPreferences();
final JsonCache jsonCache = JsonCacheMem(JsonCacheEncPrefs(encPrefs));
…
JsonCacheSecStorage #
JsonCacheSecStorage is an implementation on top of the flutter_secure_storage package.
…
final secStorage = FlutterSecureStorage(…);
final JsonCache jsonCache = JsonCacheSecStorage(secStorage);
// In order to write a string value, define it as a map key whose associated
// value is a boolean placeholder value set to 'true'.
final Map<String, dynamic> info = {'an secret info': true};
jsonCache.refresh('secret', info);
// later on…
final mappedInfo = (await jsonCache.value('secret'))!;
final originalInfo = mappedInfo.keys.first; // 'an secret info'
JsonCacheLocalStorage #
JsonCacheLocalStorage is an implementation on top of the localstorage package.
…
final LocalStorage storage = LocalStorage('my_data');
final JsonCache jsonCache = JsonCacheMem(JsonCacheLocalStorage(storage));
…
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));
…
JsonCacheCrossLocalStorage #
JsonCacheLocalCrossStorage is an implementation on top of the cross_local_storage package.
…
final LocalStorageInterface prefs = await LocalStorage.getInstance();
final JsonCache jsonCache = JsonCacheMem(JsonCacheCrossLocalStorage(prefs));
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.
Suggested Dependency Relationship #
Whenever a function, method, or class needs to interact with user data, it
should do so via a reference to the JsonCache
interface rather than relying on
an actual implementation.
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;
}
}
By depending on an interface rather than an actual implementation, the code
above is loosely coupled to this
package — which means it's easy to test as you can
mock the JsonCache
dependency.
Using Fake Implementation #
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.
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'});
});
});
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
…
}
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
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