Flutter, Dart — локальная база в сотни раз более быстрая чем SQLite или Shared Preferencies

от автора

Примерно месяц назад общаясь с одним разработчиком приложения на Flutter встала проблема торможения обработки маленьких (в десятках тысяч) массивов данных на телефоне юзера.

Многие приложения предполагают обработку данных на телефоне и, далее, их синхронизацию с бэкендом. Например: списки дел, списки каких либо данных (анализов, заметок и т.п.).

Совсем не круто, когда список всего нескольких тысяч элементов, при удалении одного из них и далее записи в кеш или при поиске по кешу — начинает тормозить.

Решение есть! Hive — noSql база, написанная на чистом Dart, очень быстрая. Кроме этого плюсы Hive:

  • Кросс-платформенность — так как на чистом Dart и нет нативных зависимостей — mobile, desktop, browser.
  • Высокая производительность.
  • Встроенное сильное шифрование.

В статье мы посмотрим как использовать Hive и сделаем простое ToDo приложение, которое в следующей статье дополним авторизацией и синхронизацией с облаком.

 

Начало

Вот такое приложение мы напишем в конце статьи, код выложен на github.

Один из плюсов Hive — очень хорошая документация https://docs.hivedb.dev/ — по идее там все есть. В статье я просто кратко опишу как с чем работать и сделаем пример.

Итак, для подключения hive в проект добавляем в pubspec.yaml

dependencies:   hive: ^1.4.1+1   hive_flutter: ^0.3.0+2  dev_dependencies:   hive_generator: ^0.7.0+2   build_runner: ^1.8.1

Далее инициализируем, обычно в начале приложения.

await Hive.initFlutter();

В случае, если мы пишем приложение под разные платформы, можно сделать инициализацию с условием. Пакет hive_flutter: ^0.3.0+2 — просто сервисная обертка упрощающая работу с Flutter.

Типы данных

Из коробки, Hive поддерживает типы данных List, Map, DateTime, BigInt и Uint8List.

Также, можно создавать свои типы и работать с ними используя адаптер TypeAdapters. Адаптеры можно делать самим https://docs.hivedb.dev/#/custom-objects/type_adapters или использовать встроенный генератор (мы его подключаем вот этой строкой hive_generator: ^0.7.0+2).

Для нашего приложения нам понадобится класс для хранения тудушек

class Todo {   bool complete;   String id;   String note;   fnal String task; }

Модифицируем его для генератора и присваиваем id — typeId: 0

import 'package:hive/hive.dart'; part 'todo.g.dart'; @HiveType(typeId: 0) class Todo {   @HiveField(0)   bool complete; @HiveField(1)   String id; @HiveField(2)   String note; @HiveField(3)   String task; }

Если в будущем нам понадобится расширить класс, то важно не нарушать порядок, а добавлять новые свойства далее по номерам. Уникальные номера свойств используются для их идентификации в бинарном формате Hive.

Теперь запускаем команду
flutter packages pub run build_runner build --delete-conflicting-outputs
и получаем сгенерированный класс для нашего типа данных todo.g.dart

Теперь мы можем использовать этот тип для записи и получения объектов этого типа в\из Hive.

HiveObject — класс для упрощения менеджмента объектов Мы можем добавить удобные методы в наш тип данных Todo просто наследовав его от встроенного класса HiveObject

class Todo extends HiveObject {

Это добавляет 2 метода save() и delete(), которые иногда удобно использовать.

Хранение данных — Box

В hive данные хранятся в боксах (box). Это очень удобно, так как мы можем сделать разные боксы для настроек юзера, тудушек и т.д.

Box идентифицируется строкой. Чтобы его использовать, надо сначала его асинхронно открыть (это очень быстрая операция). В нашем варианте
var todoBox = await Hive.openBox<Todo>('box_for_todo');
и потом мы можем синхронно read / write данные из этого бокса.

Идентификация данных в боксе возможна либо по ключу, либо по порядковому номеру:

Например, откроем бокс со строковыми данными и запишем данные по ключу и с автоинкрементом
var stringBox = await Hive.openBox<String>('name_of_the_box');

По ключу

stringBox.put('key1', 'value1'); print(stringBox.get('key1')); // value1 stringBox.put('key2', 'value2'); print(stringBox.get('key2')); // value2

Автоинкеремент при записи + доступ по индексу

Для записи множества объектов удобно использовать бокс аналогично List. У всех объектов в боксе есть индекс типа autoinrement.

Для работы с этим предназначены методы: getAt(), putAt() and deleteAt()

Для записи просто используем add() без индекса.

stringBox.put('value1'); stringBox.put('value2'); stringBox.put('value3'); print(stringBox.getAt(1)); //value2

Почему возможно работать синхронно? Разработчики пишут, что это одна сильных сторон Hive. При запросе на запись все Listeners уведомляются сразу, а запись происходит в фоне. Если произошел сбой записи (что очень маловероятно и по идее, можно это не обрабатывать), то Listeners уведомляются снова. Также можно использовать await для работы.

Когда бокс уже открыт, то в любом месте приложения мы его вызываем
var stringBox = await Hive.box<String>('name_of_the_box');
из чего сразу становится понятно, что при создании бокса пакет делает singleton.

Поэтому, если мы не знаем, открыт уже бокс или нет, а проверять лень, то можно в сомнительных местах использовать
var stringBox = await Hive.openBox<String>('name_of_the_box');

  • в случае если бокс уже открыт, этот вызов просто вернет инстанс уже открытого бокса.

Если посмотреть исходный код пакета, то можно увидеть несколько вспомогательных методов:

  /// Returns a previously opened box.   Box<E> box<E>(String name);   /// Returns a previously opened lazy box.   LazyBox<E> lazyBox<E>(String name);   /// Checks if a specific box is currently open.   bool isBoxOpen(String name);   /// Closes all open boxes.   Future<void> close();

Devhack Вообще, так как Flutter — полный open source, то можно лезть в любые методы и пакеты, что зачастую быстрее и понятнее, чем читать документацию.

LazyBox — для больших массивов данных

Когда мы создаем обычный бокс, то все его содержимое хранится в памяти. Это дает высокое быстродействие. В таких боксах удобно хранить настройки пользователя, какие-то небольшие данные.

Если данных много, то лучше создавать боксы lazily

var lazyBox = await Hive.openLazyBox('myLazyBox'); var value = await lazyBox.get('lazyVal');

При его открытии Hive считывает ключи и хранит их в памяти. Когда мы запрашиваем данные, то Hive знает положение данных на диске и быстро считывает их.

Шифрование боксов

Hive поддерживает AES-256 шифрование данных в боксе.
Для моздания 256-битного ключа мы можем использовать встроенную функцию
var key = Hive.generateSecureKey();
которая создает ключ используя Fortuna random number generator.

После создания ключа создаем бокс

var encryptedBox = await Hive.openBox('vaultBox', encryptionKey: key); encryptedBox.put('secret', 'Hive is cool'); print(encryptedBox.get('secret'));

Особенности:

  • Шифруются только значения, ключи хранятся как plaintext.
  • При закрытии приложения ключ можно хранить с помощью пакета flutter_secure_storage или использовать свои методы.
  • Нет встроенной проверки корректности ключа, поэтому, в случае неправильного ключа мы должны сами программировать поведение приложения.

Сжатие боксов

Как обычно, если мы удаляем или меняем данные, то они пишутся по нарастающей в конец бокса.

Мы можем делать сжатие, например при закрытии бокса при выходе из приложения

var box = Hive.box('myBox'); await box.compact(); await box.close();

Приложение todo_hive_example

Ок, это вроде бы все, напишем приложение, которое потом расширим для работы с бэкендом.
Модель данных у нас уже есть, интерфейс сделаем простой.

Экраны:

  • Главный экран — список дел + весь функционал
  • Экран добавления дела

Действия:

  • Кнопка + добавляет дело
  • Нажатие на галочку — переключение выполнено \ не выполнено
  • Смахивание в любую сторону — удаление

Создание приложения Действие 1 — добавить

Создаем новое приложение, удаляем комменты, всю структуру оставляем (нам понадобится кнопочка +.

Модель данных кладем в отдельную папку, запускаем команду для создания генератора.
Создаем список общих дел.

Для построения списка дел используем встроенное расширение (да, в Dart пару месяцев назад добавили расширения (extensions)), которое лежит тут /hive_flutter-0.3.0+2/lib/src/box_extensions.dart

/// Flutter extensions for boxes. extension BoxX<T> on Box<T> {   /// Returns a [ValueListenable] which notifies its listeners when an entry   /// in the box changes.   ///   /// If [keys] filter is provided, only changes to entries with the   /// specified keys notify the listeners.   ValueListenable<Box<T>> listenable({List<dynamic> keys}) =>       _BoxListenable(this, keys?.toSet()); }

Мы кстати сами можем легко создавать подобные расширения, чем займемся в усложненном варианте приложения в следующей статье.

Итак, создаем список дел, который сам будет обновляться при изменении бокса

      body: ValueListenableBuilder(         valueListenable: Hive.box<Todo>(HiveBoxes.todo).listenable(),         builder: (context, Box<Todo> box, _) {           if (box.values.isEmpty)             return Center(               child: Text("Todo list is empty"),             );           return ListView.builder(             itemCount: box.values.length,             itemBuilder: (context, index) {               Todo res = box.getAt(index);               return ListTile(                 title: Text(res.task),                 subtitle: Text(res.note),               );             },           );         },       ),

Пока список пустой. Теперь при нажатии кнопочки + сделаем добавление дела.

Для этого создаем экран с формой, на который перекидываем при нажатии кнопочки +.

На этом экране при нажатии кнопки Add вызываем код, который добавляет запись в бокс и перекидывает обратно на главный экран.

void _onFormSubmit() {     Box<Todo> contactsBox = Hive.box<Todo>(HiveBoxes.todo);     contactsBox.add(Todo(task: task, note: note));     Navigator.of(context).pop();   }

Все, первая часть готова, это приложение уже умеет добавлять Todo. При перезапуске приложения все данные сохраняются в Hive.

Коммит на github приложения на этой стадии.

Создание приложения Действие 2 — переключение выполнено \ не выполнено

Тут все совсем просто. Мы используем метод сохранения, который получили наследовав наш class Todo extends HiveObject

Заполняем два свойства и всё готово

                  leading: res.complete                       ? Icon(Icons.check_box)                       : Icon(Icons.check_box_outline_blank),                   onTap: () {                     res.complete = !res.complete;                     res.save();                   }); 

Коммит на github приложения на этой стадии.

Создание приложения Действие 3 — смахивание влево — удаление

Тут тоже все просто. Оборачиваем виджет в котором хранится дело в dismissable и опять используем сервисный метод удаления.

                background: Container(color: Colors.red),                 key: Key(res.id),                 onDismissed: (direction) {                   res.delete();                 },

Вот и все, мы получили полностью рабочее приложение, которое хранит данные в локальной базе.

Код приложения на github — https://github.com/awaik/todo_hive_example

To be continued:

  • В следующей статье прикрутим авторизацию с паттерном BLoC. Через почту, гугл, Apple Id.
  • Потом добавим синхронизацию с бэкендом Firebase через GraphQl по пути рассмотрев проблему устойчивого обновления короткоживущих токенов. Рассмотрим также устойчивую синхронизацию в обе стороны бэкенда и фронтэнда в неустойчивой среде мобильного приложения (прошу прощения за тавтологию).

ссылка на оригинал статьи https://habr.com/ru/post/498070/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *