{"id":24154818,"url":"https://github.com/PackRuble/cardoteka","last_synced_at":"2025-09-19T22:31:14.837Z","repository":{"id":213484721,"uuid":"563289712","full_name":"PackRuble/cardoteka","owner":"PackRuble","description":"The best type-safe wrapper over SharedPreferences.  ⭐  Why so?   -\u003e strongly typed cards for access to storage   -\u003e don't think about type, use get|set   -\u003e can work with nullable values   -\u003e callback based updates","archived":false,"fork":false,"pushed_at":"2025-03-11T13:45:09.000Z","size":1040,"stargazers_count":4,"open_issues_count":18,"forks_count":0,"subscribers_count":1,"default_branch":"dev","last_synced_at":"2025-04-21T20:46:50.211Z","etag":null,"topics":["cardoteka","dart","database","flutter","persistence","shared-preferences","storage"],"latest_commit_sha":null,"homepage":"https://t.me/+AkGV73kZi_Q1YTMy","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PackRuble.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-11-08T09:58:20.000Z","updated_at":"2025-03-11T13:42:05.000Z","dependencies_parsed_at":"2023-12-21T09:55:49.518Z","dependency_job_id":"55a879fc-de4a-4b84-b395-e6453f68a2c4","html_url":"https://github.com/PackRuble/cardoteka","commit_stats":null,"previous_names":["packruble/cardoteka"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/PackRuble/cardoteka","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PackRuble%2Fcardoteka","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PackRuble%2Fcardoteka/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PackRuble%2Fcardoteka/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PackRuble%2Fcardoteka/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PackRuble","download_url":"https://codeload.github.com/PackRuble/cardoteka/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PackRuble%2Fcardoteka/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276011431,"owners_count":25569837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-19T02:00:09.700Z","response_time":108,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cardoteka","dart","database","flutter","persistence","shared-preferences","storage"],"created_at":"2025-01-12T12:02:11.534Z","updated_at":"2025-09-19T22:31:14.816Z","avatar_url":"https://github.com/PackRuble.png","language":"Dart","funding_links":[],"categories":["Dart"],"sub_categories":[],"readme":"\u003ca href=\"https://github.com/PackRuble/cardoteka/\"\u003e\u003cimg src=\"https://github.com/PackRuble/cardoteka/blob/dev/res/cardoteka_banner.png?raw=true\"/\u003e\u003c/a\u003e\n\n## Cardoteka\n\n[![telegram_badge]][telegram_link]\n[![pub_badge]][pub_link]\n[![pub_likes]][pub_link]\n[![codecov_badge]][codecov_link]\n[![license_badge]][license_link]\n[![code_size_badge]][repo_link]\n[![repo_star_badge]][repo_link]\n\n⭐️ The best type-safe wrapper over SharedPreferences.\n\n\u003e 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!\n\n## Advantages\n\nWhy should I prefer to use [`cardoteka`](https://pub.dev/packages/cardoteka) instead of the original [`shared_preferences`](https://pub.dev/packages/shared_preferences)? The reasons are as follows:\n- 🎈 Easy data retrieval synchronously (based on pre-caching) or asynchronously using `Cardoteka` and `CardotekaAsync`.\n- 🧭 Your keys and default values are stored in a systematic and organized manner. You don't have to think about where to stick them.\n- 🎼 Use `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.\n- 📞 Update state as soon as new data arrives in storage. No to code duplication - use `Watcher`.\n- 🧯 Have to frequently check the value for null before saving? Use the `getOrNull` and `setOrNull` methods and don't worry about anything!\n- 🚪 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`.\n\n## Table of contents\n\n\u003c!-- TOC --\u003e\n  * [Cardoteka](#cardoteka)\n  * [Advantages](#advantages)\n  * [Table of contents](#table-of-contents)\n  * [How to use?](#how-to-use)\n  * [Materials](#materials)\n  * [Apps](#apps)\n  * [Analogy in `SharedPreferencesWithCache` and `SharedPreferencesAsync`](#analogy-in-sharedpreferenceswithcache-and-sharedpreferencesasync)\n  * [Sync or Async storage](#sync-or-async-storage)\n  * [Saving null values](#saving-null-values)\n  * [Structure](#structure)\n    * [`Cardoteka` and `CardotekaAsync`](#cardoteka-and-cardotekaasync)\n    * [`Card`](#card)\n    * [`Converter`](#converter)\n    * [`Watcher`](#watcher)\n    * [`Detachability`](#detachability)\n  * [Use with](#use-with)\n    * [`ChangeNotifier`](#changenotifier)\n    * [`ValueNotifier`](#valuenotifier)\n    * [`Cubit` (bloc)](#cubit-bloc)\n    * [`Provider` (riverpod)](#provider-riverpod)\n    * [`Notifier` (riverpod)](#notifier-riverpod)\n  * [Migration](#migration)\n    * [Cardoteka from v1 to v2](#cardoteka-from-v1-to-v2)\n  * [Obfuscate](#obfuscate)\n  * [Coverage](#coverage)\n  * [Author](#author)\n\u003c!-- TOC --\u003e\n\n## How to use?\n\n1. 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 `DataType` enumeration:\n\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter/material.dart' show ThemeMode;\n\nenum AppSettings\u003cT extends Object?\u003e implements Card\u003cT\u003e {\n  themeMode(DataType.string, ThemeMode.system),\n  recentActivityList(DataType.stringList, \u003cString\u003e[]),\n  isPremium(DataType.bool, false),\n  feedCatAtAppointedTime\u003cDateTime?\u003e(DataType.int, null),\n  ;\n\n  const AppSettings(this.type, this.defaultValue);\n\n  @override\n  final DataType type;\n\n  @override\n  final T defaultValue;\n\n  @override\n  String get key =\u003e name;\n\n  static const converters = \u003cCard, Converter\u003e{\n    themeMode: EnumAsStringConverter(ThemeMode.values),\n    feedCatAtAppointedTime: Converters.dateTimeAsInt,\n  };\n}\n```\n\n2. Select the cardoteka class required in your case - `Cardoteka` (based on pre-caching) or `CardotekaAsync` for asynchronous data retrieval (see [Sync or Async storage](https://github.com/PackRuble/cardoteka?tab=readme-ov-file#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:\n\n```dart\nFuture\u003cvoid\u003e main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  await Cardoteka.init();\n  final cardoteka = Cardoteka(\n    config: const CardotekaConfig(\n      name: 'settings',\n      cards: SettingsCards.values,\n      converters: SettingsCards.converters,\n    ),\n  );\n\n  ThemeMode themeMode = cardoteka.get(SettingsCards.themeMode);\n  print(themeMode); // will return default value -\u003e ThemeMode.light\n\n  await cardoteka.set(SettingsCards.themeMode, ThemeMode.dark);\n  themeMode = cardoteka.get(SettingsCards.themeMode);\n  print(themeMode); // ThemeMode.dark\n\n  // you can use generic type to prevent possible errors when passing arguments\n  // of different types\n  await cardoteka.set\u003cbool\u003e(SettingsCards.isPremium, true);\n  await cardoteka.set\u003cColor\u003e(SettingsCards.userColor, Colors.deepOrange);\n\n  await cardoteka.remove(SettingsCards.themeMode);\n  Map\u003cCard\u003cObject?\u003e, Object\u003e storedEntries = cardoteka.getStoredEntries();\n  print(storedEntries);\n  // {\n  //   SettingsCards.userColor: Color(0xffff5722),\n  //   SettingsCards.isPremium: true\n  // }\n\n  await cardoteka.removeAll();\n  storedEntries = cardoteka.getStoredEntries();\n  print(storedEntries); // {}\n}\n```\n\n**Don't worry!** If you do something wrong, you will receive a detailed correction message in the console.\n\n## Materials\n\nList of resources to learn more about the capabilities of this library:\n- [Stop using dynamic key-value storage! Use Cardoteka for typed access to Shared Preferences | by Ruble | Medium](https://medium.com/@pack.ruble/stop-using-dynamic-key-value-storage-use-cardoteka-for-typed-access-to-shared-preferences-567c9f799d7d)\n- [Я сделал Cardoteka и вот как её использовать [кто любит черпать] / Хабр](https://habr.com/ru/articles/783712/)\n- [Cardoteka — техническая начинка и аналитика решений типобезопасной SP [кто любит вдаваться] / Хабр](https://habr.com/ru/articles/801089/)\n- [Приложение викторины: внедрение Cardoteka и основные паттерны проектирования с Riverpod / Хабр](https://habr.com/ru/articles/799437/)\n\n## Apps\n\nApplications that use this library:\n- [Weather Today](https://github.com/PackRuble/weather_today) - weather app\n- [Quiz Prize](https://github.com/PackRuble/quiz_prize_app) - quiz game deployed on [web](https://packruble.github.io/quiz_prize_app)\n- [PackRuble/reactive_domain_playground](https://github.com/PackRuble/reactive_domain_playground) - sandbox for practicing skills in a reactive Domain layer\n\n## Analogy in `SharedPreferencesWithCache` and `SharedPreferencesAsync`\n\n| `SharedPreferencesWithCache` or `SharedPreferencesAsync` | Method \\ return signature | `Cardoteka`         | `CardotekaAsync`            |\n|----------------------------------------------------------|---------------------------|---------------------|-----------------------------|\n| `get*`                                                   | `get`                     | `V`                 | `Future\u003cV\u003e`                 |\n| —                                                        | `getOrNull`               | `V?`                | `Future\u003cV?\u003e`                |\n| `set*`                                                   | `set`                     | `Future\u003cbool\u003e`      | `Future\u003cbool\u003e`              |\n| —                                                        | `setOrNull`               | `Future\u003cbool\u003e`      | `Future\u003cbool\u003e`              |\n| `remove`                                                 | `remove`                  | `Future\u003cbool\u003e`      | `Future\u003cbool\u003e`              |\n| `clear`                                                  | `removeAll`               | `Future\u003cbool\u003e`      | `Future\u003cbool\u003e`              |\n| `containsKey`                                            | `containsCard`            | `bool`              | `Future\u003cbool\u003e`              |\n| `keys` and `getKeys`                                     | `getStoredCards`          | `Set\u003cCard\u003e`         | `Future\u003cSet\u003cCard\u003e\u003e`         |\n| —                                                        | `getStoredEntries`        | `Map\u003cCard, Object\u003e` | `Future\u003cMap\u003cCard, Object\u003e\u003e` |\n| `reloadCache`                                            | `reloadCache`             | `Future\u003cvoid\u003e`      | —                           |\n\n\n## Sync or Async storage\n\nThe 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.\n\nThings 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.\n\nBut which one to use when? It's simple: \n- if your data is updated by another service (and you can't track it)\n- OR your data is too heavy (lists with instances of classes with a large number of fields are serialized)\n- OR synchronous reading is not that important to you\n \nthen feel free to use `CardotekaAsync`. Otherwise, use `Cardoteka`.\n\n## Saving null values\n\nIf your card can contain a null value, then use the `getOrNull` and `setOrNull` methods. It works like this:\n- `getOrNull` - if pair is absent in storage, we will get `null`\n- `setOrNull` - if we save `null`, the pair will be deleted from storage\n\nBelow is a table showing the compatibility of methods with cards:\n\n|   method    | Card\u003cObject\\\u003e | Card\u003cObject?\u003e |\n|:-----------:|:-------------:|:-------------:|\n|    `get`    |       ✅       |       ❌       |\n|    `set`    |       ✅       |       ✅       |\n| `getOrNull` |       ✅       |       ✅       |\n| `setOrNull` |       ✅       |       ✅       |\n\nBy 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`.\n\n## Structure\n\nThe structure of the library is very simple! Below are the main classes you will have to work with.\n\n| Basic elements of Cardoteka      | Purpose                                              |\n|----------------------------------|------------------------------------------------------|\n| `Cardoteka` and `CardotekaAsync` | Classes for working with storage                     |\n| `CardotekaConfig`                | Configuration file for a `CardotekaCore` instance    |\n| `Card`                           | Key to the storage to interact with it               |\n| `Converter` \u0026 `Converters`       | Transforming objects to interact with storage        |\n| `Watcher`                        | Allows you to listen for changing values in storage  |\n| `Detachability`                  | Allows you to remove linked resources when listening |\n\n### `Cardoteka` and `CardotekaAsync`\n\nMain 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).\n\n| Mixin for `CardotekaCore` | Purpose                                     |\n|---------------------------|---------------------------------------------|\n| `Watcher`\u003c-`WatcherImpl`  | To implement wiretapping based on callbacks |\n| `CRUD`                    | To simulate crud operations                 |\n\nUse `import package:cardoteka/access_to_sp.dart` to access classes of the original `shared_preferences`.\n\n### `Card`\n\nEvery 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).\n\n### `Converter`\n\nConverters are used to convert your object into a simple type that can be stored in storage. There are 5 basic types available:\n\n| enum `DataType` | Basic Dart type |\n|-----------------|-----------------|\n| bool            | `bool`          |\n| int             | `int`           |\n| double          | `double`        |\n| string          | `String`        |\n| stringList      | `List\u003cString\u003e`  |\n\nIf 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.\n\n| Converter                   | Representation of an object in storage |\n|-----------------------------|----------------------------------------|\n| `Converters`                |                                        |\n| -\u003e`_ColorConverter`         | `Color` as `int`                       |\n| -\u003e`_UriConverter`           | `Uri` as `String`                      |\n| -\u003e`_DurationConverter`      | `Duration` as `int`                    |\n| -\u003e`_DateTimeConverter`      | `DateTime` as `String`                 |\n| -\u003e`_DateTimeAsIntConverter` | `DateTime` as `int`                    |\n| -\u003e`_NumConverter`           | `num` as `double`                      |\n| -\u003e`_NumAsStringConverter`   | `num` as `String`                      |\n| `Enum`                      |                                        |\n| -\u003e`EnumAsStringConverter`   | `Iterable\u003cEnum\u003e` as `String`           |\n| -\u003e`EnumAsIntConverter`      | `Iterable\u003cEnum\u003e` as `int`              |\n| `CollectionConverter`       |                                        |\n| -\u003e`IterableConverter`       | `Iterable\u003cE\u003e` as `List\u003cString\u003e`        |\n| -\u003e`ListConverter`           | `List\u003cE\u003e` as `List\u003cString\u003e`            |\n| -\u003e`MapConverter`            | `Map\u003cK, V\u003e` as `List\u003cString\u003e`          |\n\n### `Watcher`\n\nI 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:\n- `onChange` -\u003e to notify when a value is changed in storage (without comparison)\n- `onRemove` -\u003e  to notify when a value is removed from storage (`remove` or `removeAll` methods)\n- `detacher` -\u003e when listening no longer makes sense\n- `fireImmediately` -\u003e to fire `onChange` at the moment the `attach` method is called\n\nCalling 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](https://github.com/PackRuble/cardoteka/issues/38).\n\nIt is important to emphasize that you can implement your own solution based on `Watcher`.\n\n### `Detachability`\n\nThe `Detachability` functionality is the ability to clear bound resources when `attach`ing 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.\n\nThe `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)`.\n\nCheck out examples of using `Detachability` functionality with different state managers in section [\"Use with\"](https://github.com/PackRuble/cardoteka?tab=readme-ov-file#use-with).\n\n## Use with\n\nAll the most up-to-date examples can be found in the [example/lib](https://github.com/PackRuble/cardoteka/tree/dev/example/lib) folder of this project. Here are just some simple practices to use with different tools.\n\nOne common `AppCardoteka` instance and `AppSettings` cards are defined for all these cases. They look like this:\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter/material.dart' show ThemeMode;\n\nenum AppLocale { ru, de, en, pl, uk }\n\nenum HomePageState { open, closed, minimized, unknown }\n\nenum AppSettings\u003cT extends Object?\u003e implements Card\u003cT\u003e {\n  themeMode(DataType.string, ThemeMode.system),\n  recentActivityList(DataType.stringList, \u003cString\u003e[]),\n  isPremium(DataType.bool, false),\n  homePageState(DataType.string, HomePageState.unknown),\n  appLocale(DataType.string, AppLocale.en),\n  feedCatAtAppointedTime\u003cDateTime?\u003e(DataType.int, null),\n  ;\n\n  const AppSettings(this.type, this.defaultValue);\n\n  @override\n  final DataType type;\n\n  @override\n  final T defaultValue;\n\n  @override\n  String get key =\u003e name;\n\n  static const converters = \u003cCard, Converter\u003e{\n    themeMode: EnumAsStringConverter(ThemeMode.values),\n    homePageState: EnumAsStringConverter(HomePageState.values),\n    appLocale: EnumAsStringConverter(AppLocale.values),\n    feedCatAtAppointedTime: Converters.dateTimeAsInt,\n  };\n}\n\nfinal class AppCardoteka = Cardoteka with WatcherImpl;\nfinal appCardoteka = AppCardoteka(\n  config: const CardotekaConfig(\n    name: 'app_settings',\n    cards: AppSettings.values,\n    converters: AppSettings.converters,\n  ),\n);\n```\n\n### `ChangeNotifier`\n\nThere 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:\n\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter/material.dart';\n\nimport 'app_cardoteka.dart';\n\n// I created an instance of Cardoteka and cards earlier, and here I'm just\n// showing you their types and uses\nfinal AppCardoteka cardoteka = appCardoteka;\nconst AppSettings\u003cList\u003cString\u003e\u003e card = AppSettings.recentActivityList;\n\n/// An example of using [Cardoteka] with the [WatcherImpl] and\n/// [DetacherChangeNotifier] mixins for the [ChangeNotifier] state class.\nclass ActivityNotifier with ChangeNotifier, DetacherChangeNotifier {\n  ActivityNotifier() {\n    cardoteka.attach(\n      card,\n      onChange: (value) {\n        recentActivity = value;\n        notifyListeners();\n      },\n      onRemove: () {\n        recentActivity.clear();\n        notifyListeners();\n      },\n      detacher: onDetach,\n      fireImmediately: true,\n    );\n  }\n\n  List\u003cString\u003e recentActivity = [];\n\n  void addActivity(String text) =\u003e\n      cardoteka.set(card, [...recentActivity, text]);\n\n  void removeActivities() =\u003e cardoteka.remove(card);\n}\n```\n\nImportant! 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](https://github.com/PackRuble/cardoteka/blob/dev/example/lib/change_notifier_with_cardoteka.dart).\n\n```dart\nclass _RecentActivityAppState extends State\u003cRecentActivityApp\u003e {\n  final _activityNR = ActivityNotifier();\n  final _textCR = TextEditingController();\n\n  @override\n  void dispose() {\n    _activityNR.dispose();\n    _textCR.dispose();\n    super.dispose();\n  }\n\n  void addRecord() {\n    _activityNR.addActivity(_textCR.text);\n    _textCR.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // ...\n    ListenableBuilder(\n      listenable: _activityNR,\n      builder: (context, child) =\u003e ListView(\n        children: [\n          for (final activity in _activityNR.recentActivity.reversed)\n            Text(activity),\n        ],\n      ),\n    );\n    // ...\n    TextButton(\n      onPressed: _activityNR.removeActivities,\n      child: const Text('Delete all records'),\n    );\n    // ...\n    TextField(\n      controller: _textCR,\n      onEditingComplete: addRecord,\n    );\n    // ...\n    IconButton.filledTonal(\n      onPressed: addRecord,\n      icon: const Icon(Icons.add),\n    );\n    // ...\n    \n    return MaterialApp(/*...*/);\n  }\n}\n```\n\n![](https://github.com/PackRuble/cardoteka/blob/dev/res/changenotifier_with_cardoteka.png)\n\n### `ValueNotifier`\n\nEverything 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:\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter/material.dart';\n\nimport 'app_cardoteka.dart';\n\n// I created an instance of Cardoteka and cards earlier, and here I'm just\n// showing you their types and uses\nfinal AppCardoteka cardoteka = appCardoteka;\nconst AppSettings\u003cbool\u003e card = AppSettings.isPremium; // with defaultValue=false\n\n/// An example of using [Cardoteka] with the [WatcherImpl] and\n/// [DetacherChangeNotifier] mixins for the [ValueNotifier] state class.\nclass PremiumNotifier extends ValueNotifier\u003cbool\u003e with DetacherChangeNotifier {\n  PremiumNotifier(super.isPremium);\n\n  Future\u003cvoid\u003e checkPremium() async {\n    final bool result = await Future.delayed(\n      // simulate server request delay\n      const Duration(milliseconds: 100),\n      () =\u003e true,\n    );\n\n    await cardoteka.set(card, result);\n  }\n}\n```\n\nWe still need the `DetacherChangeNotifier` mixin for proper resource utilization. Now:\n```dart\nFuture\u003cvoid\u003e main() async {\n  await Cardoteka.init();\n\n  // We get a previously saved value from storage.\n  // If isn't present, `card.defaultValue` will be returned.\n  final isPremium = cardoteka.get(card);\n  final premiumNR = PremiumNotifier(isPremium);\n  print('1️⃣State is premium?: value=${premiumNR.value}');\n\n  cardoteka.attach(\n    card,\n    onChange: (value) =\u003e premiumNR.value = value,\n    onRemove: () =\u003e premiumNR.value = card.defaultValue,\n    detacher: premiumNR.onDetach, // a line that allows you to fix memory leaks\n  );\n\n  await premiumNR.checkPremium();\n  print('2️⃣State is premium?: value=${premiumNR.value}');\n\n  await cardoteka.set(card, false);\n  print('3️⃣State is premium?: value=${premiumNR.value}');\n\n  premiumNR.dispose();\n}\n```\n\nWhat happened?\n1. Get current value from storage by card\n2. console-\u003e 1️⃣State is premium?: value=false\n3. Attach a watcher to this card, which will notify the notifier about new values\n4. Check premium on the server by calling `PremiumNotifier.checkPremium` method\n5. console-\u003e 2️⃣State is premium?: value=true\n6. We save the new value to cardoteka, and after triggering watcher..:\n7. console-\u003e 3️⃣State is premium?: value=false\n\nThat 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.\n\n### `Cubit` (bloc)\n\nThis is about using it in conjunction with the [bloc](https://pub.dev/packages/bloc) package. First we need to implement \"detachability\" (there are several options, all see [here](https://github.com/PackRuble/cardoteka/blob/dev/example/lib/cubit_with_cardoteka.dart)). It is more convenient if you determine it in the \"general\" place and will be used everywhere:\n\n```dart\nimport 'package:bloc/bloc.dart' show Cubit;\nimport 'package:cardoteka/cardoteka.dart' show Detachability;\nimport 'package:meta/meta.dart' show mustCallSuper;\n\n/// Second implementation of [Detachability] from `cardoteka` package. Copy.\nmixin DetacherCubitV2\u003cT\u003e on Cubit\u003cT\u003e implements Detachability {\n  final _detachability = Detachability();\n\n  @override\n  void onDetach(void Function() cb) =\u003e _detachability.onDetach(cb);\n\n  @override\n  void detach() =\u003e _detachability.detach();\n\n  @override\n  @mustCallSuper\n  Future\u003cvoid\u003e close() async {\n    detach();\n    return super.close();\n  }\n}\n```\n\nNow let's create a cubit that will be responsible for the theme of our application:\n```dart\nimport 'package:bloc/bloc.dart';\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter/material.dart' show ThemeMode;\n\nimport 'app_cardoteka.dart';\n\n// I created an instance of Cardoteka and cards earlier, and here I'm just\n// showing you their types and uses\nfinal AppCardoteka cardoteka = appCardoteka;\nconst AppSettings\u003cThemeMode\u003e card =\n    AppSettings.themeMode; // with defaultValue=ThemeMode.system\n\nclass CubitThemeMode extends Cubit\u003cThemeMode\u003e with DetacherCubitV2 {\n  CubitThemeMode(super.initialState);\n\n  void onNewTheme(ThemeMode value) =\u003e emit(value);\n}\n\nFuture\u003cvoid\u003e main() async {\n  await Cardoteka.init();\n\n  final themeMode = cardoteka.get(card);\n\n  final cubit = CubitThemeMode(themeMode);\n  cardoteka.attach(\n    card,\n    onChange: cubit.onNewTheme,\n    onRemove: () =\u003e cubit.onNewTheme(card.defaultValue),\n    detacher: cubit.onDetach, // a line that allows you to fix memory leaks\n  );\n\n  await cardoteka.set\u003cThemeMode\u003e(card, ThemeMode.light);\n}\n```\n\nWhat happened?\n1. Get current `themeMode` from storage by card\n2. Create `CubitThemeMode` with actual `themeMode`\n3. Attach a watcher to this card, which will notify the `CubitImpl` about new values\n4. We save the new value to cardoteka, and after triggering watcher...\n4. What does the `onNewTheme` method call...\n5. And `CubitThemeMode` emit new state `ThemeMode.light`.\n\n### `Provider` (riverpod)\n\nThis is about using it in conjunction with the [riverpod](https://pub.dev/packages/riverpod) package. First, you need to create a \"Cardoteka\" provider for your storage instance and your desired state provider:\n\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:riverpod/riverpod.dart';\n\nimport 'app_cardoteka.dart';\n\n// I created an instance of Cardoteka and cards earlier, and here I'm just\n// showing you their types and uses\nfinal cardotekaProvider = Provider((_) =\u003e appCardoteka);\nconst AppSettings\u003cHomePageState\u003e card =\n    AppSettings.homePageState; // with defaultValue=HomePageState.unknown\n\nfinal homePageStateProvider = Provider\u003cHomePageState\u003e(\n  (ref) =\u003e ref.watch(cardotekaProvider).attach(\n    card,\n    onChange: (value) =\u003e ref.state = value,\n    onRemove: () =\u003e ref.state = HomePageState.unknown,\n    detacher: ref.onDispose,\n  ),\n);\n```\n\nNote 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.\n\nThe usage code will look like this:\n\n```dart\nFuture\u003cvoid\u003e main() async {\n  await Cardoteka.init();\n  final container = ProviderContainer();\n  final cardoteka = container.read(cardotekaProvider);\n\n  HomePageState homePageState = container.read(homePageStateProvider);\n  print('$homePageState'); // card.defaultValue-\u003e HomePageState.unknown\n\n  await cardoteka.set(card, HomePageState.open);\n  homePageState = container.read(homePageStateProvider);\n  print('$homePageState');\n  // 1. a value was saved to storage\n  // 2. the `onChange` callback we passed to `attach` is called.\n  // 3. print-\u003e HomePageState.open\n\n  await cardoteka.remove(card);\n  homePageState = container.read(homePageStateProvider);\n  print('$homePageState');\n  // 1. a value was removed from storage\n  // 2. the function we passed to `onRemove` is called.\n  // 3. print-\u003e HomePageState.unknown\n}\n```\n\n### `Notifier` (riverpod)\n\nThis is about using it in conjunction with the [riverpod](https://pub.dev/packages/riverpod) package. Create a notifier to work with the current locale:\n\n```dart\nimport 'package:cardoteka/cardoteka.dart';\nimport 'package:flutter_riverpod/flutter_riverpod.dart';\nimport 'package:flutter/material.dart';\n\nimport 'app_cardoteka.dart';\n\n// I created an instance of Cardoteka and cards earlier, and here I'm just\n// showing you their types and uses\nfinal cardotekaProvider = Provider((_) =\u003e appCardoteka);\nconst AppSettings\u003cAppLocale\u003e card =\n    AppSettings.appLocale; // with defaultValue=AppLocale.en\n\nclass LocaleNotifier extends Notifier\u003cAppLocale\u003e {\n  static final i =\n    NotifierProvider\u003cLocaleNotifier, AppLocale\u003e(LocaleNotifier.new);\n\n  late AppCardoteka _storage;\n\n  @override\n  AppLocale build() {\n    _storage = ref.watch(cardotekaProvider);\n\n    return _storage.attach(\n      card,\n      onChange: (value) =\u003e state = value,\n      detacher: ref.onDispose,\n      onRemove: () =\u003e state = card.defaultValue,\n    );\n  }\n\n  Future\u003cvoid\u003e changeLocale(AppLocale locale) async =\u003e\n      await _storage.set(card, locale);\n\n  Future\u003cvoid\u003e resetLocale() async =\u003e await _storage.remove(card);\n}\n```\n\nNote how convenient it is to pass `ref.onDispose` to `detacher` when `attach`ing a listener to a storage: as soon as the current notifier is disposed of, it will also clear the associated resources in storage. \n\nBelow is the simplest application to change the locale of your application:\n\n```dart\nFuture\u003cvoid\u003e main() async {\n  await Cardoteka.init();\n  runApp(const ProviderScope(child: LocaleSelectorApp()));\n}\n\nclass LocaleSelectorApp extends ConsumerWidget {\n  const LocaleSelectorApp({super.key});\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final localePR = LocaleNotifier.i;\n    final localeNR = ref.watch(localePR.notifier);\n    final locale = ref.watch(localePR);\n\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      home: Scaffold(\n        body: Column(\n          children: [\n            const Spacer(),\n            Center(\n              child: Text(\n                locale.localizedName,\n                style: Theme.of(context).textTheme.displaySmall,\n              ),\n            ),\n            const Spacer(),\n            DropdownMenu(\n              initialSelection: locale,\n              dropdownMenuEntries: [\n                for (final locale in AppLocale.values)\n                  DropdownMenuEntry(\n                    label: '$locale',\n                    value: locale,\n                  ),\n              ],\n              onSelected: (value) {\n                if (value == null) return;\n                localeNR.changeLocale(value);\n              },\n            ),\n            Padding(\n              padding: const EdgeInsets.all(8.0),\n              child: TextButton(\n                onPressed: localeNR.resetLocale,\n                child: const Text('Reset locale'),\n              ),\n            ),\n            const Spacer(),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nextension AppLocaleX on AppLocale {\n  String get localizedName =\u003e switch (this) {\n        AppLocale.ru =\u003e 'Русский',\n        AppLocale.en =\u003e 'English',\n        AppLocale.uk =\u003e 'Українська',\n        AppLocale.pl =\u003e 'Polski',\n        AppLocale.de =\u003e 'Deutsch',\n      };\n}\n```\n\nWith this mini application, we can select locale, see localized text, and reset locale. And thanks to the `attach`ed `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](https://github.com/PackRuble/cardoteka/blob/dev/example/lib/riverpod_provider_cardoteka.dart).\n\nThe `AsyncNotifier` is used in the same way.\n\n## Migration\n\n### Cardoteka from v1 to v2\n\nI tried to make the transition from version 1 as gentle as possible AND still very productive on new features. \n\n1. **All declarations of own classes from `Cardoteka` and `CardotekaAsync` (new) must now necessarily be declared as `final` or `base` or `sealed`.**\n\n2. **The `Watcher.attach` method is updated.**\n\nBefore:\n```dart\ncardoteka.attach(\n  card,\n  (value) {/* Do something with the new value */},\n  onRemove: () {/* Was optional */},\n);\n```\n\nAfter:\n```dart\ncardoteka.attach(\n  card,\n  onChange: (value) {/* This parameter is now named */},\n  onRemove: () {/* This parameter is now required */},\n);\n```\n\n3. **The `AccessToSP` has been deleted.** Use `import package:cardoteka/access_to_sp.dart`.\n\n4. **Data migration**\n\nMigration version 2 must be carried out if:\n- you previously used `cardoteka` package version `1.*.*`;\n- you previously used `shared_preferences` version `2.3.0` and lower;\n\nThen do the following and it will automatically migrate your data:\n```dart\nawait CardotekaMigrator.migrate();\n// and then the usual actions\nawait Cardoteka.init();\n```\nBy default, all your entries from the old version of the storage will be moved to the new one (old storage will be cleared). \n\nIf you need more control over the process, you can define your own handler for each entry:\n```dart\nawait CardotekaMigrator.migrate(\n  toV2Handler: (key, value) =\u003e (key, value, removeOld: false, ignore: false),\n);\n```\n\nIf you don't need migration, then either don't call this method, or do this:\n```dart\nawait CardotekaMigrator.migrate(toV2Handler: null);\n```\n\nThe result `HandlerEntryV2` of executing `toV2Handler` shows what should be done with the given entry.\n\n`HandlerEntryV2` represents a record resulting from the execution of a data migration handler.\nHer parameters:\n- `key` a new key associated with a value that will be stored in storage.\n- `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.\n- `removeOld` allows you to delete an entry from the old storage.\n- `ignore` completely ignores this entry.\n\n*Let's deal with some special cases*. Suppose you needed:\n- `fcm_vapid_key` save in new storage and leave in old storage\n- `platform_available_memory_mb` ignore for new storage and leave in old storage\n- `theme_mode_index` change name to 'user_settings.themeModeApp' and change value from `1` to `light` and delete from old storage\n\nMoreover, 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. \n\nAnd if your configuration looks like this:\n```dart\nconst config = CardotekaConfig(\n  name: 'user_settings',\n  cards: [/* .., StorageCard.themeModeApp .., */],\n);\n```\n\nIn addition to this, you have used Cardoteka before and there are also keys (cards) stored there that will simply be move to new storage:\n- `user_settings.isPremium`\n- `user_settings.userName`\n\nEverything in general can be done like this:\n```dart\nawait CardotekaMigrator.migrate(\n  toV2Handler: (key, value) =\u003e switch (key) {\n    'fsm_vapid_key' =\u003e (key, value, removeOld: false, ignore: false),\n    'platform_available_memory_mb' =\u003e (key, value, removeOld: false, ignore: true),\n    'theme_mode_index' =\u003e (\n        '${config.name}.themeModeApp',\n        switch (value) {\n          1 =\u003e ThemeMode.light,\n          2 =\u003e ThemeMode.dark,\n          _ =\u003e ThemeMode.system,\n        }\n            .name,\n        removeOld: true,\n        ignore: false,\n      ),\n    // for all other keys\n    _ =\u003e (key, value, removeOld: true, ignore: false),\n  },\n);\n// It was before migration in old storage:\n// {\n//   'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',\n//   'platform_available_memory_mb': 2119.3,\n//   'theme_mode_index': 1,\n// };\n// and at the same time in new storage:\n// {\n//   'user_settings.isPremium': true,\n//   'user_settings.userName': 'Ivan',\n// };\n//\n//\n// Now after migration in old storage:\n// {\n//   'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',\n//   'platform_available_memory_mb': 2119.3,\n// };\n// and in new storage:\n// {\n//   'fsm_vapid_key': 'BKagOny0KF_2pCJQ3mmoL0ewzQ8rZu',\n//   'user_settings.themeModeApp': 'light',\n//   'user_settings.isPremium': true,\n//   'user_settings.userName': 'Ivan',\n//   '_cardoteka_package_did_migrate_v2': true,\n// };\n```\n\nNote! Depending on the platform, the old and new storage may overlap.\nThis method potentially takes this into account.\n\nThe migration will result in an entry with the `_cardoteka_package_did_migrate_v2` key\nin storage about the status of the current migration.\n\n## Obfuscate\n\nAt the time of writing, the [documentation](https://docs.flutter.dev/deployment/obfuscate#caveat) states that obfuscation does not apply to `Enum`:\n\n\u003e Enum names are not obfuscated currently.\n\nHowever, this behavior may change in the future. So for now you can safely use `String get key =\u003e name;` as keys for your cards.\n\n## Coverage\n\nThe 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. \n\n[codecov_tree_badge]: https://codecov.io/gh/PackRuble/cardoteka/graphs/tree.svg\n[codecov_sunburst_badge]: https://codecov.io/gh/PackRuble/cardoteka/graphs/sunburst.svg\n\n| [![codecov_tree_badge]][codecov_link] | [![codecov_sunburst_badge]][codecov_link] |\n|---------------------------------------|-------------------------------------------|\n\n## Author\n\nYou can contact me or check out my activities on the following platforms:\n\n- [Github](https://github.com/PackRuble)\n- [Telegram Group](https://t.me/+AkGV73kZi_Q1YTMy)\n- [StackOverflow](https://stackoverflow.com/users/17991131/ruble)\n- [Medium](https://medium.com/@pack.ruble)\n- [Habr](https://habr.com/ru/users/PackRuble/)\n\n\u003e 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❣️\n\u003e \n\u003e With respect to everyone involved, Ruble.\n\n[telegram_badge]: https://img.shields.io/badge/Telegram_channel-❤️-_?style=plastic\u0026logo=telegram\u0026color=33cccc\n[telegram_link]: https://t.me/+AkGV73kZi_Q1YTMy\n\n[pub_likes]: https://img.shields.io/pub/likes/cardoteka?style=plastic\u0026logo=flutter\u0026color=c24641\n[pub_badge]: https://img.shields.io/pub/v/cardoteka.svg?style=plastic\u0026logo=dart\n[pub_link]: https://pub.dev/packages/cardoteka\n\n[codecov_badge]: https://img.shields.io/codecov/c/github/PackRuble/cardoteka?style=plastic\u0026color=00cc00\u0026logo=codecov\n[codecov_link]: https://app.codecov.io/gh/PackRuble/cardoteka\n\n[license_badge]: https://img.shields.io/github/license/PackRuble/cardoteka?style=plastic\u0026logo=apache\u0026color=996600\n[license_link]: https://github.com/PackRuble/cardoteka/blob/dev/LICENSE\n\n[code_size_badge]: https://img.shields.io/github/languages/code-size/PackRuble/cardoteka?style=plastic\u0026color=339966\n[repo_link]: https://github.com/PackRuble/cardoteka\n\n[repo_star_badge]: https://img.shields.io/github/stars/packruble/cardoteka?style=plastic\u0026logo=github\u0026color=DAA520\n[repo_star_link]: https://github.com/PackRuble/cardoteka/network/dependents\n\n[pub_like_icon]: https://pub.dev/static/hash-ffjootqp/img/like-active.svg","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPackRuble%2Fcardoteka","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FPackRuble%2Fcardoteka","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPackRuble%2Fcardoteka/lists"}