Movatterモバイル変換


[0]ホーム

URL:


Хабрβ
Как стать автором
Обновить
PackRuble

Я сделал Cardoteka и вот как её использовать [кто любит черпать]

Уровень сложностиПростой
Время на прочтение11 мин
Количество просмотров2.2K
Обзор

Привет! Самое время в предновогоднее настоящее поделиться с вами опенсУрс проектом :) Встречайте ->Cardoteka <- строгая типизированная обёртка над Shared Preferences (SP) в мире Flutter. Этот материал будет коротким, с рекламными нотками (а точнее, приглашающий к дискуссии в issues и в комментарии) и readme-подтекстом. Так или иначе, это заслуженная метка "Обзор".

cardoteka | Flutter Package
pub.dev

https://github.com/PackRuble/cardoteka

Обозначу в первую очередь пару вещей:

  1. У проекта есть подробный README, который я намерен продублировать в некоторой степени

  2. Естьсамодокументированный код. Серьёзно, над этим велась работа - это не пустой текст ради баллов в pub.dev

  3. Есть тесты, если вы любите такое познание

Подробный анализ "зачем и почему" готовится в виде отдельной публикации - технической, объёмной и серьёзной (насколько позволяет моя серьёзность). Но чтобы заинтересовать читателя и оправдать появление текущей статьи, а также указать на важность существования этой "обёртки над SP", заявляю: (с префиксом "теперь мы можем")

  • упорядоченно храним ключ и значение по умолчанию. Легко создаём новую пару, точно уверены в её типах

  • вытекающий приятный бонус в использовании всего лишь двух методовget|set (или CRUD методов), чтобы получить/сохранить значение любого типа

  • nullable значения - не помеха! Имитируем поведение и используем методыgetOrNull|setOrNull

  • умеем слушать поступление/удаление значения из хранилища и реактивно обновлять состояния

?Добро пожаловать, всё расскажу!

Содержание

  • Начало использования

    • Карта/карточка - это пара "ключ-значение"

    • Свой экземпляр cardoteka - свои правила

    • `get` и `set` - ловись рыбка большая и маленькая

  • Маленькое FAQ

    • А всё ли Я указал правильно?

    • Хочется использовать методы CRUD?

    • Мне нужна сырая Shared Preferences!

  • Один приятный бонус - реактивная прослушка

  • Заключение

Начало использования

Работу скардотекой (да, есть смысловая связь со словом "Картотека") следует начинать с понимания её сущностей.

Карта/карточка - это пара "ключ-значение"

В первую очередь у нас есть место, где хранятся все парыключ-значение_по_умолчанию_для_данного_ключа - это можно представить в виде enum или class:

import 'package:cardoteka/cardoteka.dart';import 'package:flutter/material.dart' hide Card;enum SettingsCards<T extends Object> implements Card<T> {   userColor(DataType.int, Color(0x00FF4BFF)),   themeMode(DataType.string, ThemeMode.light),   isPremium(DataType.bool, false),   ;   const SettingsCards(this.type, this.defaultValue);   @override   final DataType type;   @override   final T defaultValue;   @override   String get key => name;   static Map<SettingsCards, Converter> get converters => const {      themeMode: EnumAsStringConverter(ThemeMode.values),      userColor: Converters.colorAsInt,   };}

Пожалуй, самая важная строка здесь

Здесь ключ выводится на основе имени перечисления, однако это может быть подвержено ошибкам: ваше имя после использования не должно измениться. Если изменится - вы потеряли доступ к вашей паре (имя вашего enum-значения это ключ в хранилище. Изменится имя -> изменится ключ -> старое значение становится недоступным).

Варианты:

Далее,DataType - это перечисление доступных типов,в которое (to) сконвертируется ваше значение внутри кардотеки. Тут всё просто - укажите значение по умолчанию и подумайте, в какой тип из доступных оно может быть преобразовано. Если типы совпадают, то ничего делать дополнительно не нужно. В примереSettingsCards.isPremium имеет тип bool, а значит конвертация не требуется. Доступные типы соответствуют оригинальным методам из SP и могут быть следующими:

Но не переживайте, сохраним всё! Есть куча конвертеров, чтобы преобразование доставляло радость (или по крайней мере - не доставляло хлопот). Достаточно где-то завести переменную (в моём случае это статический геттерconverters в классе перечисления, что удобно, поскольку убирает бойлерплейт имени класса), где указать пару "карта-конвертер". Все доступные конвертеры вы можете увидеть ниже:

Стоит обратить внимание, что для использования конвертеров коллекций необходимо создать собственный экземпляр класса с расширением от желаемого конвертера и указать тип элемента. Одна строка кода - и далее используйте встроенные возможностиquick fix от dart-анализатора. Остаётся указать, как мы хотим [де]сериализовывать объект (например в json-формат):

Приятен тот факт, что заложенная гибкость позволяет использовать полностью свой класс-конвертер. Просто реализуйте интерфейс классаConverter или расширьтесь отCollectionConverter для коллекций (под капотом всё тот жеConverter).

Хотя и всё это подразумевалось как мини-обзор, укажу для полноты руководства про значение по умолчанию и про nullable, потому что это одна из важных единиц функциональности библиотеки.

Если мы используем перечисление для создания списка карт, то значение по умолчанию должно быть константой (всякие трюки с геттерами и switch не рассматриваю). Если желаемый класс, который мы хотим сохранить, не имеет константного конструктора, то это может стать проблемой. Однако есть пару возможностей для преодоления этого барьера:

Свой экземпляр cardoteka - свои правила

Это интересное место, поскольку определяет ваш стиль игры :) Вы можете знать, что оригинальный классSharedPreferences - это синглтон. Обёртка в виде классаCardoteka позволяет вам иметь свои экземпляры, где с одной стороны хранилище всё ещё остаётся синглтоном (статичное приватное поле_prefs внутри), а с другой стороны конфигурационный классCardotekaConfig не позволяет влиять разным экземплярам кардотеки друг на друга.

Вау, что за архитектура! А потому что "костылирование, инкостыляция и поликостылизм" наше всё! Шучу конечно, я старался не использовать эти технологии. Ладно, вместо долгих снов, вам нужно расшириться отCardoteka и... всё!:

class SettingsCardoteka extends Cardoteka with WatcherImpl {  SettingsCardoteka({required super.config});}

Дополнительно скажу, что можно приправить наш собственный класс вкусняшками черезwith (да, вместо добавления функциональности привычной агрегацией я проникся миксинами. Есть и техническая сторона вопроса, расскажу во второй части):

Пока что оставляю это без особого (и заслуженного!) внимания и перехожу к параметруconfig. Лучшим вариантом будет рассмотреть его в процессе инициализации самой кардотеки. Сколько бы экземпляров кардотеки вы не планировали создать, инициализацию достаточно сделать один раз (не забываем, SP синглтон ведь):

main() async {  await Cardoteka.init();  final cardoteka = SettingsCardoteka(    config: CardotekaConfig(      name: 'settings',      cards: SettingsCards.values,      converters: SettingsCards.converters,    ),  );    // ... и теперь пользуемся}

Конфигурационный файл принимает имя (в конечном счёте, это ещё одна зона разделения самих экземпляров кардотеки - имя используется в качестве префикса для ключей), коллекцию карточек и конвертеры для них. То есть в самом SP ключ будет выглядеть так -settings.SettingsCards.имя_значения_перечисления, а если для каждой карточки использовать кастомный ключ, тоsettings.кастомный_ключ. Да и всё, что тут ещё говорить - давайте делать запросы!

get и set - ловись рыбка большая и маленькая

В двух словах, логика работы с кардотекой: чтобы получить значение, вам потребуется передать карточку. Если в хранилище ничего не было, то вернётся значение по умолчанию. Так или иначе, вы точно не получите null, а ваш тип будет соответствовать установленному типу в карточке для значения по умолчанию. Анализатор Dart всё выведет сам ?

Чтобы установить значение, также передаём карточку И новое значение для этой карточки. Если всё пройдёт успешно, вернётся bool в значенииtrue. Здесь есть важный нюанс, который поможет избежать runtime-ошибок: когда вы делаетеset илиsetOrNull, указывайте дженерик тип. Это защита от самого себя - если вы укажете один тип карты (имеется ввиду тип именно значения по умолчанию для этой карты), а сохранить по ней попытаетесь значение другого типа, то произойдёт ошибка. Если указываем generic, то схитрить уже не получится - анализатор будет требовать от аргументов указанный тип.

main() async {  // инициализация cardoteka...  ThemeMode themeMode = cardoteka.get(SettingsCards.themeMode);  print(themeMode); // будет возвращено значение по умолчанию -> ThemeMode.light  await cardoteka.set(SettingsCards.themeMode, ThemeMode.dark);  themeMode = cardoteka.get(SettingsCards.themeMode);  print(themeMode); // ThemeMode.dark  // вы можете использовать правильный generic-тип, чтобы предотвратить возможные  // ошибки, когда случайно указываются аргументы разных типов.  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); // {}}

Остальные операции соответствуют SP, разве что вместо ключа вы передаёте карточку, где это требуется: удаление -remove, удаление всего -removeAll, проверить наличие пары в хранилище -containsCard, получить хранимые карты -getStoredCards, получить хранимые пары -getStoredEntries. Эти операции индивидуальны для каждого экземпляра кардотеки (если в передаваемой конфигурации разное имя ИЛИ в том числе и разное имя класса перечисления, я думаю о возможности добавить assert) и никак не затрагивают другие экземпляры.

Всё немного интересней, когда мы касаемся работы с null. SP не поддерживает сохранение null-значений. Всё, что мы можем сделать, это сымитировать такую работу. Поэтому, если ваша карточка может содержать null-значение, то воспользуйтесь методамиgetOrNull иsetOrNull. Работает это так:

И тот и другой метод вы можете использовать и с не-null-евыми карточками. Резонный вопрос: с какими типами карточек может работать метод получения/сохранения? Проще всего представить это в виде таблицы:

По большому счёту, чаще всего вы будете использоватьget/set, а когда необходима симуляция работы с null, либо же в момент отсутствия пары хочется получитьnull (а не значение по умолчанию) - используемgetOrNull/setOrNull.

Маленькое FAQ

А всё ли Я указал правильно?

Этот вопрос, как и другие подобного характера:

Корректные ли типы я указал? Нужен ли для этой карточки конвертер? А правильно ли он конвертирует? Есть ли дубликаты ключей?

решён! Переживать не стоит, так как если что-то указано неправильно или чего-то не хватает для работы Cardotekи, то вы получите подробное сообщение в консоль о том, как это исправить. ?

Хочется использовать методы CRUD?

Легко, дополните ваш класс Cardoteka миксиномCRUD и вы получите любимыеread,create,update,delete. При этом вашем распоряжении остаются и стандартные методы для работы с хранилищем.

Мне нужна сырая Shared Preferences!

Вероятно вам нужен доступ для динамического изменения ключей или же вы мягко вкатываетесь в типизированную структуру карточек... Используйте миксинAccessToSP и получайте доступ к "сырому"SharedPreferences prefs.

Если вы в процессе тестирования, используйтеCardotekaUtilsForTest. Там есть привычныйsetMockInitialValues и ещё немножко полезных методов.

Один приятный бонус - реактивная прослушка

Скажу без сомнений, что реализация этой штуки была одной из самых времязатратных и важных для меня. Хотя кажется, что нижеописанная концепция вредит архитектуре, но с другой стороны она позволяет быстро связать бизнес-состояние с хранилищем реактивной нитью. Что должно быть тепло воспринято в мире MVP.

Покажу самый банальный пример на основеChangeNotifier - это то, что входит в Flutter SDK "с завода". Наша цель проста: при сохранении значения в хранилище мы хотим реактивного обновления состояния нотифаера.

Наш нотифаер будет выглядеть следующим образом (бизнес-логика незаурядна: добавляем каждый новый заказ в список, а самый последний заказ всегда остаётся в хранилище. Если последний заказ удаляется из хранилища, то очищаем список-состояние):

import 'package:cardoteka/cardoteka.dart';import 'package:flutter/material.dart' hide Card;class OrderNotifier with ChangeNotifier, Detachability {  final _orders = <String>[];  void addOrder(String value) {    _orders.add(value);    notifyListeners();    print('New order: $value');  }    void removeAll() {    _orders.clear();    print('_orders has been cleared!');  }}

Да, кстати, пожалуйста, перестаньте расширяться отChangeNotifier. В этом нет необходимости, но так сложилось исторически:ChangeNotifer появился в библиотеке раньше, чем появились миксины в языке. Для полного погружения в ситуацию смотрите этот issue -Allow mixins in "extends" clauses · Issue #1942 · dart-lang/language.

Теперь самое время продемонстрировать работоспособность решения. Первым делом нам понадобится экземпляр Cardoteka с примиксованнымWatcherImpl. Этот класс даёт нам полезный методattach, с помощью которого можно передать callback для прослушивания новых значений:

class CardotekaImpl = Cardoteka with WatcherImpl;Future<void> main() async {  await Cardoteka.init();  // ignore_for_file: definitely_unassigned_late_local_variable  // to☝️do: процесс инициализации опущен  late CardotekaImpl cardoteka;  late Card<String> lastOrderCard;  final notifier = OrderNotifier();  cardoteka.attach(     lastOrderCard,     notifier.addOrder,     onRemove: notifier.removeAll,     detacher: notifier.onDispose,  );  await cardoteka.set(lastOrderCard, '#341');  // 1. значение было сохранено в хранилище  // 2. console-> New order: #341    await cardoteka.remove(lastOrderCard);  // 3. значение было удалено из хранилища  // 4. console-> _orders has been cleared!}

Из кода должно быть всё ясно.., кромеDetachability, который на данный момент существует только в моей голове. Его реализация видится мне такой:

import 'package:flutter/foundation.dart' show VoidCallback;mixin Detachability on ChangeNotifier {  List<VoidCallback>? _onDisposeCallbacks;  void onDetach(void Function() cb) {    _onDisposeCallbacks ??= [];    _onDisposeCallbacks!.add(cb);  }  @override  void dispose() {    _onDisposeCallbacks?.forEach((cb) => cb.call());    _onDisposeCallbacks = null;super.dispose();  }}

Надеюсь, вы окончательно запутаны! Если нет, то поздравляю, с архитектурой вы на очень плотное ТЫ. Суть этой примеси в том, что необходимо предоставить возможность любому классу хранить список из detach-callback'ов, которые он вызовет, когда экземплярChangeNotifier станет ненужным. Это очистит связанные ресурсы внутриWatcherImpl.

РеализацияDetachability функциональности подлежит всеобщему обсуждению в этих issues:

Соединения с другими BLoC-хранящими классами (ChangeNotifier,ValueNotifier,Cubit из пакетаbloc,Provider из пакетаriverpod) есть в readme проектав этой главе. Пока что вы можете просто скопировать вышеуказанный код и использовать в своём проекте. Как только реализация будет стандартизирована и протестирована, обновление не заставит себя ждать.

И да, еслиWactherImpl не нравится по тем или иным причинам, то можно сделать свой собственныйBlackjackWatcher на стримах... или на чём позабористей.

Заключение

Хочу выразить искреннюю благодарность тем, кто был со мной ментально в момент создания этой маленькой обёртки для нужд всех желающих. Это была первая часть, маленькая user-frendly шалость, обзор на ночь для крепкого сна.

Я буду нескрываемо рад, если сообщество примет участие в обсуждении дальнейшего развития проекта: поделится мнением об удобстве использования, расскажет о технических нюансах или просто пожелает что-нибудь к Новому Году ?

Код доступен под лицензией Apache-2.0 ⭐

С наступающим Новым Годом, друзья! ?

© 2022-2024 Ruble


Ссылки:

  1. Issues · PackRuble/cardoteka

  2. PackRuble/cardoteka: The best type-safe wrapper over SharedPreferences

  3. cardoteka | Flutter Package

Всего голосов 6: ↑6 и ↓0+6
10
Карма
0
Рейтинг

Моя литера Т стремится к равнозначности линий

Отправить сообщение

Публикации

Истории

Работа

iOS разработчик
11 вакансий
Swift разработчик
15 вакансий

Ближайшие события

4 – 5 апреля
Геймтон «DatsCity»
Онлайн
8 апреля
Конференция TEAMLY WORK MANAGEMENT 2025
МоскваОнлайн
Больше событий в календаре
Разработка
Администрирование
Менеджмент
Больше событий в календаре
Аналитика
Тестирование
Другое
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
Больше событий в календаре
Разработка
Маркетинг
Другое
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область

[8]ページ先頭

©2009-2025 Movatter.jp