Cardoteka
The best type-safe wrapper over SharedPreferences. ⭐ Why so? -> strongly typed cards for access to storage -> don't think about type, use get|set -> can work with nullable values -> callback based updates
Install / Use
/learn @PackRuble/CardotekaREADME
<a href="https://github.com/PackRuble/cardoteka/"><img src="https://github.com/PackRuble/cardoteka/blob/dev/res/cardoteka_banner.png?raw=true"/></a>
Cardoteka
[![telegram_badge]][telegram_link] [![pub_badge]][pub_link] [![pub_likes]][pub_link] [![codecov_badge]][codecov_link] [![license_badge]][license_link] [![code_size_badge]][repo_link] [![repo_star_badge]][repo_link]
⭐️ The best type-safe wrapper over SharedPreferences.
Put a ![][pub_like_icon] on [Pub][pub_link] and favorite ⭐ on [Github][repo_link] to keep up with changes and not miss new releases!
Advantages
Why should I prefer to use cardoteka instead of the original shared_preferences? The reasons are as follows:
- 🎈 Easy data retrieval synchronously (based on pre-caching) or asynchronously using
CardotekaandCardotekaAsync. - 🧭 Your keys and default values are stored in a systematic and organized manner. You don't have to think about where to stick them.
- 🎼 Use
getorsetinstead of a heap ofgetBool,setDouble,getInt,getStringList,setString... Think about the business logic of entities, not how to store or retrieve them. - 📞 Update state as soon as new data arrives in storage. No to code duplication - use
Watcher. - 🧯 Have to frequently check the value for null before saving? Use the
getOrNullandsetOrNullmethods and don't worry about anything! - 🚪 Do you still need access to dynamic methods or an SP instance from the original library? Just add the
import package:cardoteka/access_to_sp.dart.
Table of contents
<!-- TOC -->- Cardoteka
- Advantages
- Table of contents
- How to use?
- Materials
- Apps
- Analogy in
SharedPreferencesWithCacheandSharedPreferencesAsync - Sync or Async storage
- Saving null values
- Structure
- Use with
- Migration
- Obfuscate
- Coverage
- Author
How to use?
- Define your cards: specify the type to be stored and the default value (for default values with nullable support, be sure to specify generic type). Additionally, specify converters if the value type cannot be represented in the existing
DataTypeenumeration:
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter/material.dart' show ThemeMode;
enum AppSettings<T extends Object?> implements Card<T> {
themeMode(DataType.string, ThemeMode.system),
recentActivityList(DataType.stringList, <String>[]),
isPremium(DataType.bool, false),
feedCatAtAppointedTime<DateTime?>(DataType.int, null),
;
const AppSettings(this.type, this.defaultValue);
@override
final DataType type;
@override
final T defaultValue;
@override
String get key => name;
static const converters = <Card, Converter>{
themeMode: EnumAsStringConverter(ThemeMode.values),
feedCatAtAppointedTime: Converters.dateTimeAsInt,
};
}
- Select the cardoteka class required in your case -
Cardoteka(based on pre-caching) orCardotekaAsyncfor asynchronous data retrieval (see Sync or Async storage). For theCardotekaclass, perform initialization viaCardoteka.initand take advantage of all the features of your cardoteka: save, read, delete, listen to your saved data using typed cards:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Cardoteka.init();
final cardoteka = Cardoteka(
config: const CardotekaConfig(
name: 'settings',
cards: SettingsCards.values,
converters: SettingsCards.converters,
),
);
ThemeMode themeMode = cardoteka.get(SettingsCards.themeMode);
print(themeMode); // will return default value -> ThemeMode.light
await cardoteka.set(SettingsCards.themeMode, ThemeMode.dark);
themeMode = cardoteka.get(SettingsCards.themeMode);
print(themeMode); // ThemeMode.dark
// you can use generic type to prevent possible errors when passing arguments
// of different types
await cardoteka.set<bool>(SettingsCards.isPremium, true);
await cardoteka.set<Color>(SettingsCards.userColor, Colors.deepOrange);
await cardoteka.remove(SettingsCards.themeMode);
Map<Card<Object?>, Object> storedEntries = cardoteka.getStoredEntries();
print(storedEntries);
// {
// SettingsCards.userColor: Color(0xffff5722),
// SettingsCards.isPremium: true
// }
await cardoteka.removeAll();
storedEntries = cardoteka.getStoredEntries();
print(storedEntries); // {}
}
Don't worry! If you do something wrong, you will receive a detailed correction message in the console.
Materials
List of resources to learn more about the capabilities of this library:
- Stop using dynamic key-value storage! Use Cardoteka for typed access to Shared Preferences | by Ruble | Medium
- Я сделал Cardoteka и вот как её использовать [кто любит черпать] / Хабр
- Cardoteka — техническая начинка и аналитика решений типобезопасной SP [кто любит вдаваться] / Хабр
- Приложение викторины: внедрение Cardoteka и основные паттерны проектирования с Riverpod / Хабр
Apps
Applications that use this library:
- Weather Today - weather app
- Quiz Prize - quiz game deployed on web
- PackRuble/reactive_domain_playground - sandbox for practicing skills in a reactive Domain layer
Analogy in SharedPreferencesWithCache and SharedPreferencesAsync
| SharedPreferencesWithCache or SharedPreferencesAsync | Method \ return signature | Cardoteka | CardotekaAsync |
|----------------------------------------------------------|---------------------------|---------------------|-----------------------------|
| get* | get | V | Future<V> |
| — | getOrNull | V? | Future<V?> |
| set* | set | Future<bool> | Future<bool> |
| — | setOrNull | Future<bool> | Future<bool> |
| remove | remove | Future<bool> | Future<bool> |
| clear | removeAll | Future<bool> | Future<bool> |
| containsKey | containsCard | bool | Future<bool> |
| keys and getKeys | getStoredCards | Set<Card> | Future<Set<Card>> |
| — | getStoredEntries | Map<Card, Object> | Future<Map<Card, Object>> |
| reloadCache | reloadCache | Future<void> | — |
Sync or Async storage
The biggest difference between Cardoteka and CardotekaAsync is where the data is stored when the application is running. In the synchronous case, all data for all cards are loaded once into the device RAM after calling Cardoteka.init. This is why you can use methods such as get, getOrNull, containsCard, getStoredCards, getStoredEntries synchronously. It is also important to understand that if another service on the platform changes your data, you need to call Cardoteka.reloadCache to update it before retrieving it via a Cardoteka instance.
Things are different for CardotekaAsync because data is asynchronously requested from disk when any method is called. This is why initialization is not required in advance. And because of this, you get the most up-to-date data for any query.
But which one to use when? It's simple:
- if your data is updated by another service (and you can't track it)
- OR your data is too heavy (lists with instances of classes with a large number of fields are serialized)
- OR synchronous reading is not that important to you
then feel free to use CardotekaAsync. Otherwise, use Cardoteka.
Saving null values
If your card can contain a null value, then use the getOrNull and setOrNull methods. It works like this:
getOrNull- if pair is absent in storage, we will getnullsetOrNull- if we savenull, the pair will be deleted from storage
Below is a table showing the compatibility of methods with cards:
| method | Card<Object> | Card<Object?> |
|:-----------:|:-------------:|:-------------:|
| get | ✅ |
