cardoteka
A melhor envoltório tipado seguro sobre SharedPreferences. Por quê? -> cartões fortemente tipados para acesso ao armazenamento -> use get|set -> pode funcionar com valores nulos -> atualizações baseadas em callback
O melhor envoltório seguro por tipo sobre SharedPreferences. ⭐ Por quê? -> cartões fortemente tipados para acesso ao armazenamento -> não pense em tipo, use get|set -> pode trabalhar com valores nulos -> atualizações baseadas em callback
{"sdk":"flutter"}^2.5.3^1.18.0{"sdk":"flutter"}^2.8.0anyTexto original em inglês. Visite o GitHub para a versão atual.
https://github.com/PackRuble/cardoteka/blob/dev/res/cardoteka_banner.png?raw=true
telegram_badge pub_badge pub_likes codecov_badge license_badge code_size_badge repo_star_badge
⭐️ The best type-safe wrapper over SharedPreferences.
Put a https://pub.dev/static/hash-ffjootqp/img/like-active.svg on Pub and favorite ⭐ on Github to keep up with changes and not miss new releases!
Why should I prefer to use cardoteka instead of the original shared_preferences? The reasons are as follows:
Cardoteka and CardotekaAsync.get or set instead of a heap of getBool, setDouble, getInt, getStringList, setString... Think about the business logic of entities, not how to store or retrieve them.Watcher.getOrNull and setOrNull methods and don't worry about anything!import package:cardoteka/access_to_sp.dart.SharedPreferencesWithCache and SharedPreferencesAsyncDataType enumeration: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,
};
}
Cardoteka (based on pre-caching) or CardotekaAsync for asynchronous data retrieval (see Sync or Async storage). For the Cardoteka class, perform initialization via Cardoteka.init and 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.
List of resources to learn more about the capabilities of this library:
Applications that use this library:
SharedPreferencesWithCache and SharedPreferencesAsyncSharedPreferencesWithCache 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> |
— |
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:
then feel free to use CardotekaAsync. Otherwise, use Cardoteka.
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 get nullsetOrNull - if we save null, the pair will be deleted from storageBelow is a table showing the compatibility of methods with cards:
| method | Card<Object> | Card<Object?> |
|---|---|---|
get |
✅ | ❌ |
set |
✅ | ✅ |
getOrNull |
✅ | ✅ |
setOrNull |
✅ | ✅ |
By and large, most often you will use get/set, and when you need to simulate working with null, or when there is no pair, you want to get null (and not the default value) - we use getOrNull/ setOrNull.
The structure of the library is very simple! Below are the main classes you will have to work with.
| Basic elements of Cardoteka | Purpose |
|---|---|
Cardoteka and CardotekaAsync |
Classes for working with storage |
CardotekaConfig |
Configuration file for a CardotekaCore instance |
Card |
Key to the storage to interact with it |
Converter & Converters |
Transforming objects to interact with storage |
Watcher |
Allows you to listen for changing values in storage |
Detachability |
Allows you to remove linked resources when listening |
Cardoteka and CardotekaAsyncMain class for implementing your own storage instance. Contains all the basic methods for working with SharedPreferences in a typed style. Serves as a wrapper over SP. Use as many implementations (and instances) as needed, passing a unique name in the parameters. Use mixins to extend functionality. Use Cardoteka for synchronous data reading (pre-caching via init), and CardotekaAsync for asynchronous data access (without cache).
Mixin for CardotekaCore |
Purpose |
|---|---|
Watcher<-WatcherImpl |
To implement wiretapping based on callbacks |
CRUD |
To simulate crud operations |
Use import package:cardoteka/access_to_sp.dart to access classes of the original shared_preferences.
CardEvery instance of Cardoteka needs cards. The card contains the characteristics of your key (name, default value, type) that is used to access the storage. It is convenient to implement using the enum enumeration, but you can also use the usual class, which is certainly less convenient and more error-prone. Important: Card.name is used as a key within the SP, so if the name is changed, the data will be lost (virtually, but not physically).
ConverterConverters are used to convert your object into a simple type that can be stored in storage. There are 5 basic types available:
enum DataType |
Basic Dart type |
|---|---|
| bool | bool |
| int | int |
| double | double |
| string | String |
| stringList | List<String> |
If the default value type specified in the card is not the Dart base type, you must provide the converter as a parameter when creating the Cardoteka instance. You can create your own converter based on the Converter class by implementing it. For collections, use CollectionConverter by extending it (or use Converter). However, many converters are already provided out of the box, including for collections.
| Converter | Representation of an object in storage |
|---|---|
Converters |
|
->_ColorConverter |
Color as int |
->_UriConverter |
Uri as String |
->_DurationConverter |
Duration as int |
->_DateTimeConverter |
DateTime as String |
->_DateTimeAsIntConverter |
DateTime as int |
->_NumConverter |
num as double |
->_NumAsStringConverter |
num as String |
Enum |
|
->EnumAsStringConverter |
Iterable<Enum> as String |
->EnumAsIntConverter |
Iterable<Enum> as int |
CollectionConverter |
|
->IterableConverter |
Iterable<E> as List<String> |
->ListConverter |
List<E> as List<String> |
->MapConverter |
Map<K, V> as List<String> |
WatcherI will mention Watcher and its implementation WatcherImpl separately. This is a very nice option that allows you to update your state based on the attached callback. The most important method is attach. Its essence is the ability to attach a onChange callback that will be triggered whenever a value is stored (set or setOrNull methods) in the storage. As parameters, you can specify:
onChange -> to notify when a value is changed in storage (without comparison)onRemove -> to notify when a value is removed from storage (remove or removeAll methods)detacher -> when listening no longer makes sensefireImmediately -> to fire onChange at the moment the attach method is calledCalling the attach method returns the actual value from storage OR the default value by card if none exists in storage. For CardotekaAsync, this method will first return the default value, and then return the actual value after the asynchronous operation is performed. Therefore, the fireImmediately flag is only relevant for Cardoteka instances. This behavior may change, keep an eye on The Watcher.attach for CardotekaAsync instance first value returns a default value · Issue #38 · PackRuble/cardoteka.
It is important to emphasize that you can implement your own solution based on Watcher.
DetachabilityThe Detachability functionality is the ability to clear bound resources when attaching and listening in a particular case is no longer needed. This has the same function as close in the bloc package, the dispose method in widgets and controllers, and the ref.onDispose method in the riverpod package. However, the Detachability mixin itself does not know how to clean up resources, but only contains a convenient onDetach method for storing callbacks and a detach method for deleting them later.
The DetacherChangeNotifier is a special case to be used in conjunction with ChangeNotifier for convenient use of the onDetach method in conjunction with Watcher.attach(detacher: onDetach).
Check out examples of using Detachability functionality with different state managers in section "Use with".
All the most up-to-date examples can be found in the example/lib folder of this project. Here are just some simple practices to use with different tools.
One common AppCardoteka instance and AppSettings cards are defined for all these cases. They look like this:
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter/material.dart' show ThemeMode;
enum AppLocale { ru, de, en, pl, uk }
enum HomePageState { open, closed, minimized, unknown }
enum AppSettings<T extends Object?> implements Card<T> {
themeMode(DataType.string, ThemeMode.system),
recentActivityList(DataType.stringList, <String>[]),
isPremium(DataType.bool, false),
homePageState(DataType.string, HomePageState.unknown),
appLocale(DataType.string, AppLocale.en),
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),
homePageState: EnumAsStringConverter(HomePageState.values),
appLocale: EnumAsStringConverter(AppLocale.values),
feedCatAtAppointedTime: Converters.dateTimeAsInt,
};
}
final class AppCardoteka = Cardoteka with WatcherImpl;
final appCardoteka = AppCardoteka(
config: const CardotekaConfig(
name: 'app_settings',
cards: AppSettings.values,
converters: AppSettings.converters,
),
);
ChangeNotifierThere are several architectural options for using Cardoteka in conjunction with ChangeNotifier. Below we will consider the variant in which the Cardoteka.attach binding is used in the class constructor:
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter/material.dart';
import 'app_cardoteka.dart';
// I created an instance of Cardoteka and cards earlier, and here I'm just
// showing you their types and uses
final AppCardoteka cardoteka = appCardoteka;
const AppSettings<List<String>> card = AppSettings.recentActivityList;
/// An example of using [Cardoteka] with the [WatcherImpl] and
/// [DetacherChangeNotifier] mixins for the [ChangeNotifier] state class.
class ActivityNotifier with ChangeNotifier, DetacherChangeNotifier {
ActivityNotifier() {
cardoteka.attach(
card,
onChange: (value) {
recentActivity = value;
notifyListeners();
},
onRemove: () {
recentActivity.clear();
notifyListeners();
},
detacher: onDetach,
fireImmediately: true,
);
}
List<String> recentActivity = [];
void addActivity(String text) =>
cardoteka.set(card, [...recentActivity, text]);
void removeActivities() => cardoteka.remove(card);
}
Important! Use DetacherChangeNotifier to properly dispose of all related data and pass onDetach method to detacher parameter of attach method. Next in widget I'll show you highlights, and you can find the full code at example here.
class _RecentActivityAppState extends State<RecentActivityApp> {
final _activityNR = ActivityNotifier();
final _textCR = TextEditingController();
@override
void dispose() {
_activityNR.dispose();
_textCR.dispose();
super.dispose();
}
void addRecord() {
_activityNR.addActivity(_textCR.text);
_textCR.clear();
}
@override
Widget build(BuildContext context) {
// ...
ListenableBuilder(
listenable: _activityNR,
builder: (context, child) => ListView(
children: [
for (final activity in _activityNR.recentActivity.reversed)
Text(activity),
],
),
);
// ...
TextButton(
onPressed: _activityNR.removeActivities,
child: const Text('Delete all records'),
);
// ...
TextField(
controller: _textCR,
onEditingComplete: addRecord,
);
// ...
IconButton.filledTonal(
onPressed: addRecord,
icon: const Icon(Icons.add),
);
// ...
return MaterialApp(/*...*/);
}
}
https://github.com/PackRuble/cardoteka/blob/dev/res/changenotifier_with_cardoteka.png
ValueNotifierEverything is very similar (and not surprising) to example with ChangeNotifier. However, we will consider a different architectural technique and take attach-connection outside the notifier. Let's define a notifier to implement the business logic to manage a user's premium subscription:
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter/material.dart';
import 'app_cardoteka.dart';
// I created an instance of Cardoteka and cards earlier, and here I'm just
// showing you their types and uses
final AppCardoteka cardoteka = appCardoteka;
const AppSettings<bool> card = AppSettings.isPremium; // with defaultValue=false
/// An example of using [Cardoteka] with the [WatcherImpl] and
/// [DetacherChangeNotifier] mixins for the [ValueNotifier] state class.
class PremiumNotifier extends ValueNotifier<bool> with DetacherChangeNotifier {
PremiumNotifier(super.isPremium);
Future<void> checkPremium() async {
final bool result = await Future.delayed(
// simulate server request delay
const Duration(milliseconds: 100),
() => true,
);
await cardoteka.set(card, result);
}
}
We still need the DetacherChangeNotifier mixin for proper resource utilization. Now:
Future<void> main() async {
await Cardoteka.init();
// We get a previously saved value from storage.
// If isn't present, `card.defaultValue` will be returned.
final isPremium = cardoteka.get(card);
final premiumNR = PremiumNotifier(isPremium);
print('1️⃣State is premium?: value=${premiumNR.value}');
cardoteka.attach(
card,
onChange: (value) => premiumNR.value = value,
onRemove: () => premiumNR.value = card.defaultValue,
detacher: premiumNR.onDetach, // a line that allows you to fix memory leaks
);
await premiumNR.checkPremium();
print('2️⃣State is premium?: value=${premiumNR.value}');
await cardoteka.set(card, false);
print('3️⃣State is premium?: value=${premiumNR.value}');
premiumNR.dispose();
}
What happened?
PremiumNotifier.checkPremium methodThat is, roughly speaking, we can have very many notifiers with callbacks attached that will automatically update the state after the values in the storage change.
Cubit (bloc)This is about using it in conjunction with the bloc package. First we need to implement "detachability" (there are several options, all see here). It is more convenient if you determine it in the "general" place and will be used everywhere:
import 'package:bloc/bloc.dart' show Cubit;
import 'package:cardoteka/cardoteka.dart' show Detachability;
import 'package:meta/meta.dart' show mustCallSuper;
/// Second implementation of [Detachability] from `cardoteka` package. Copy.
mixin DetacherCubitV2<T> on Cubit<T> implements Detachability {
final _detachability = Detachability();
@override
void onDetach(void Function() cb) => _detachability.onDetach(cb);
@override
void detach() => _detachability.detach();
@override
@mustCallSuper
Future<void> close() async {
detach();
return super.close();
}
}
Now let's create a cubit that will be responsible for the theme of our application:
import 'package:bloc/bloc.dart';
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter/material.dart' show ThemeMode;
import 'app_cardoteka.dart';
// I created an instance of Cardoteka and cards earlier, and here I'm just
// showing you their types and uses
final AppCardoteka cardoteka = appCardoteka;
const AppSettings<ThemeMode> card =
AppSettings.themeMode; // with defaultValue=ThemeMode.system
class CubitThemeMode extends Cubit<ThemeMode> with DetacherCubitV2 {
CubitThemeMode(super.initialState);
void onNewTheme(ThemeMode value) => emit(value);
}
Future<void> main() async {
await Cardoteka.init();
final themeMode = cardoteka.get(card);
final cubit = CubitThemeMode(themeMode);
cardoteka.attach(
card,
onChange: cubit.onNewTheme,
onRemove: () => cubit.onNewTheme(card.defaultValue),
detacher: cubit.onDetach, // a line that allows you to fix memory leaks
);
await cardoteka.set<ThemeMode>(card, ThemeMode.light);
}
What happened?
themeMode from storage by cardCubitThemeMode with actual themeModeCubitImpl about new valuesonNewTheme method call...CubitThemeMode emit new state ThemeMode.light.Provider (riverpod)This is about using it in conjunction with the riverpod package. First, you need to create a "Cardoteka" provider for your storage instance and your desired state provider:
import 'package:cardoteka/cardoteka.dart';
import 'package:riverpod/riverpod.dart';
import 'app_cardoteka.dart';
// I created an instance of Cardoteka and cards earlier, and here I'm just
// showing you their types and uses
final cardotekaProvider = Provider((_) => appCardoteka);
const AppSettings<HomePageState> card =
AppSettings.homePageState; // with defaultValue=HomePageState.unknown
final homePageStateProvider = Provider<HomePageState>(
(ref) => ref.watch(cardotekaProvider).attach(
card,
onChange: (value) => ref.state = value,
onRemove: () => ref.state = HomePageState.unknown,
detacher: ref.onDispose,
),
);
Note that using StateProvider is not necessary because the state change will occur automatically when the value in the store changes. Note also that we specify a callback in onRemove to update the provider state the moment the key-value pair is removed from storage.
The usage code will look like this:
Future<void> main() async {
await Cardoteka.init();
final container = ProviderContainer();
final cardoteka = container.read(cardotekaProvider);
HomePageState homePageState = container.read(homePageStateProvider);
print('$homePageState'); // card.defaultValue-> HomePageState.unknown
await cardoteka.set(card, HomePageState.open);
homePageState = container.read(homePageStateProvider);
print('$homePageState');
// 1. a value was saved to storage
// 2. the `onChange` callback we passed to `attach` is called.
// 3. print-> HomePageState.open
await cardoteka.remove(card);
homePageState = container.read(homePageStateProvider);
print('$homePageState');
// 1. a value was removed from storage
// 2. the function we passed to `onRemove` is called.
// 3. print-> HomePageState.unknown
}
Notifier (riverpod)This is about using it in conjunction with the riverpod package. Create a notifier to work with the current locale:
import 'package:cardoteka/cardoteka.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'app_cardoteka.dart';
// I created an instance of Cardoteka and cards earlier, and here I'm just
// showing you their types and uses
final cardotekaProvider = Provider((_) => appCardoteka);
const AppSettings<AppLocale> card =
AppSettings.appLocale; // with defaultValue=AppLocale.en
class LocaleNotifier extends Notifier<AppLocale> {
static final i =
NotifierProvider<LocaleNotifier, AppLocale>(LocaleNotifier.new);
late AppCardoteka _storage;
@override
AppLocale build() {
_storage = ref.watch(cardotekaProvider);
return _storage.attach(
card,
onChange: (value) => state = value,
detacher: ref.onDispose,
onRemove: () => state = card.defaultValue,
);
}
Future<void> changeLocale(AppLocale locale) async =>
await _storage.set(card, locale);
Future<void> resetLocale() async => await _storage.remove(card);
}
Note how convenient it is to pass ref.onDispose to detacher when attaching a listener to a storage: as soon as the current notifier is disposed of, it will also clear the associated resources in storage.
Below is the simplest application to change the locale of your application:
Future<void> main() async {
await Cardoteka.init();
runApp(const ProviderScope(child: LocaleSelectorApp()));
}
class LocaleSelectorApp extends ConsumerWidget {
const LocaleSelectorApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final localePR = LocaleNotifier.i;
final localeNR = ref.watch(localePR.notifier);
final locale = ref.watch(localePR);
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Column(
children: [
const Spacer(),
Center(
child: Text(
locale.localizedName,
style: Theme.of(context).textTheme.displaySmall,
),
),
const Spacer(),
DropdownMenu(
initialSelection: locale,
dropdownMenuEntries: [
for (final locale in AppLocale.values)
DropdownMenuEntry(
label: '$locale',
value: locale,
),
],
onSelected: (value) {
if (value == null) return;
localeNR.changeLocale(value);
},
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton(
onPressed: localeNR.resetLocale,
child: const Text('Reset locale'),
),
),
const Spacer(),
],
),
),
);
}
}
extension AppLocaleX on AppLocale {
String get localizedName => switch (this) {
AppLocale.ru => 'Русский',
AppLocale.en => 'English',
AppLocale.uk => 'Українська',
AppLocale.pl => 'Polski',
AppLocale.de => 'Deutsch',
};
}
With this mini application, we can select locale, see localized text, and reset locale. And thanks to the attached onChange callback, all you need to do is save/delete a value in storage so that state of ALL notifiers is updated in a timely manner. All this makes it possible to use a large number of notifiers and not worry that some of them are left with an irrelevant state. Check the launch of this application here.
The AsyncNotifier is used in the same way.
I tried to make the transition from version 1 as gentle as possible AND still very productive on new features.
All declarations of own classes from Cardoteka and CardotekaAsync (new) must now necessarily be declared as final or base or sealed.
The Watcher.attach method is updated.
Before:
cardoteka.attach(
card,
(value) {/* Do something with the new value */},
onRemove: () {/* Was optional */},
);
After:
cardoteka.attach(
card,
onChange: (value) {/* This parameter is now named */},
onRemove: () {/* This parameter is now required */},
);
The AccessToSP has been deleted. Use import package:cardoteka/access_to_sp.dart.
Data migration
Migration version 2 must be carried out if:
cardoteka package version 1.*.*;shared_preferences version 2.3.0 and lower;Then do the following and it will automatically migrate your data:
await CardotekaMigrator.migrate();
// and then the usual actions
await Cardoteka.init();
By default, all your entries from the old version of the storage will be moved to the new one (old storage will be cleared).
If you need more control over the process, you can define your own handler for each entry:
await CardotekaMigrator.migrate(
toV2Handler: (key, value) => (key, value, removeOld: false, ignore: false),
);
If you don't need migration, then either don't call this method, or do this:
await CardotekaMigrator.migrate(toV2Handler: null);
The result HandlerEntryV2 of executing toV2Handler shows what should be done with the given entry.
HandlerEntryV2 represents a record resulting from the execution of a data migration handler.
Her parameters:
key a new key associated with a value that will be stored in storage.value a new value associated with a key that will be stored in storage. If the HandlerEntryV2.value is null, no write to the new storage will occur.removeOld allows you to delete an entry from the old storage.ignore completely ignores this entry.Let's deal with some special cases. Suppose you needed:
fcm_vapid_key save in new storage and leave in old storageplatform_available_memory_mb ignore for new storage and leave in old storagetheme_mode_index change name to 'user_settings.themeModeApp' and change value from 1 to light and delete from old storageMoreover, we would like to use the user_settings.themeModeApp key later on as Card with Cardoteka. To do this, add the name specified in CardotekaConfig and the dot . to the key in the prefix.
And if your configuration looks like this:
const config = CardotekaConfig(
name: 'user_settings',
cards: [/* .., StorageCard.themeModeApp .., */],
);
In addition to this, you have used Cardoteka before and there are also keys (cards) stored there that will simply be move to new storage:
user_settings.isPremiumuser_settings.userNameEverything in general can be done like this:
await CardotekaMigrator.migrate(
toV2Handler: (key, value) => switch (key) {
'fsm_vapid_key' => (key, value, removeOld: false, ignore: false),
'platform_available_memory_mb' => (key, value, removeOld: false, ignore: true),
'theme_mode_index' => (
'${config.name}.themeModeApp',
switch (value) {
1 => ThemeMode.light,
2 => ThemeMode.dark,
_ => ThemeMode.system,
}
.name,
removeOld: true,
ignore: false,
),
// for all other keys
_ => (key, value, removeOld: true, ignore: false),
},
);
// It was before migration in old storage:
// {
// 'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',
// 'platform_available_memory_mb': 2119.3,
// 'theme_mode_index': 1,
// };
// and at the same time in new storage:
// {
// 'user_settings.isPremium': true,
// 'user_settings.userName': 'Ivan',
// };
//
//
// Now after migration in old storage:
// {
// 'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',
// 'platform_available_memory_mb': 2119.3,
// };
// and in new storage:
// {
// 'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',
// 'user_settings.themeModeApp': 'light',
// 'user_settings.isPremium': true,
// 'user_settings.userName': 'Ivan',
// '_cardoteka_package_did_migrate_v2': true,
// };
Note! Depending on the platform, the old and new storage may overlap. This method potentially takes this into account.
The migration will result in an entry with the _cardoteka_package_did_migrate_v2 key
in storage about the status of the current migration.
At the time of writing, the documentation states that obfuscation does not apply to Enum:
Enum names are not obfuscated currently.
However, this behavior may change in the future. So for now you can safely use String get key => name; as keys for your cards.
The most important "core" is covered by the tests part and all the places that needed covering in my opinion. There are badges at the very beginning of the current file where you can see the percentage of coverage, among other things. Or, click on the image below. It's relevant for releases.
| codecov_tree_badge | codecov_sunburst_badge |
|---|
You can contact me or check out my activities on the following platforms:
Stop thinking that something is impossible. Make your dreams come true! Move towards your goal as if the quality of your code depends on it! And of course, use good libraries❣️
With respect to everyone involved, Ruble.