“А не добавить ли в приложение локальную базу данных?” — подумала я и начала искать подходящую библиотеку для реализации. И тут мне попадается библиотека Flutter Localstorage с реализацией JSON для хранения данных. Однако она оказывается сложнее, чем другая уже известная мне библиотека Hive, на которой я и остановила свой выбор.
Сразу признаюсь, что я ни в коем случае не претендую на звание эксперта Flutter — просто учусь, как и вы. В данной статье я попыталась внести свой скромный вклад и упростить новичкам процесс изучения Flutter и базы данных Hive.
Что такое Hive?
Hive — это легковесная и молниеносная база данных на основе пар “ключ-значение”, написанная на чистом Dart в духе Bitcask.
Более подробная информация предоставляется в официальной документации Hive.
Почему Hive?
Библиотека обладает следующими характеристиками:
- кроссплатформенность (работает на мобильных устройствах, настольных компьютерах и в браузерах);
- высокая производительность (судя по результатам соответствующего теста);
- простой, эффективный и понятный API;
- встроенное надежное кодирование;
- отсутствие нативных зависимостей;
Работать с базой данных Hive — одно сплошное удовольствие, поскольку она несложная, быстрая и эффективная. Hive предназначена для локального хранения данных. Вы быстро в ней разберетесь благодаря ее простоте.
В данной статье мы пройдем следующие этапы в процессе создания реального приложения:
- обзор инструкций для чтения и записи данных в базу Hive;
- настройка проекта;
- создание
TypeAdapter
; - запись и чтение данных;
- удаление данных.
Перед тем как обратиться к примерам, рассмотрим необходимые инструкции от Hive.
Предварительные требования
Вы должны обладать базовыми знаниями Flutter: уметь создавать на нем проекты, иметь представление о виджетах и т. д.
Запись данных в базу
Hive хранит все данные в боксах (box). Проще говоря, бокс подобен таблице в SQL без какой-либо структуры и может содержать что угодно. Hive позволяет создавать несколько боксов для организации данных. Кроме того, в них можно хранить закодированные данные для защиты конфиденциальной информации, что целесообразно в крупных приложениях.
Перед началом записи в бокс необходимо его открыть:
var box = await Hive.openBox<E>(‘boxName’);
E
означает необязательный параметр типа. Он указывает на тип значения в боксе.
Есть еще Hive.openLazyBox()
. Он применяется для объемных баз данных, так как не загружает все данные в память. Но поскольку мы работаем с простым примером, воспользуемся обычным боксом. С дополнительной информацией о Lazy box можно ознакомиться в документации.
Запись данных в базу осуществляется разными способами:
var box = Hive.box('myBox');
box.put('key', 'value');
// ключ должен быть строкой string или целым числом int, а значение может быть любым объектом (int, // String, List, Map и т.д.).
box.put('cites', ['Berlin', 'Vienna', 'Hamburg']);
box.put(123, 'Wow its a number');
box.putAll({'key1': 'value1', 1 : 'one'})
// применение для существующего индекса
box.putAt(int index, 'value');
// автоматическое создание нового индекса
box.add('value');
Как видим, ключи должны быть либо string
, либо int
.
В примере используется инструкция box.add()
. Будет проще, если Hive позаботится об индексе, а мы — о входных данных. Такой ход облегчает процесс итерации по всем данным с помощью конструктора ListView
.
Чтение из базы данных
Как показано ниже, процесс чтения из базы данных не представляет сложности:
var box = Hive.box('myBox'); // инструкция .get возвращает сохраненные данные по заданному ключу // возвращает значение по умолчанию в случае отсутствия ключа String name = box.get('key', defaultValue: -1); // применение для существующего индекса String name = box.getAt(int index);
Теперь вы понимаете, почему была нужна инструкция box.add()
. Она автоматически создает индекс int
при каждом добавлении данных. Мы можем считывать их напрямую, просто указав определенный индекс в box.getAt()
. Когда дойдем до примера, где мы задействуем эту инструкцию для добавления и чтения данных, будет понятнее.
Удаление данных из базы Hive
С удалением данных также все просто:
var box = Hive.box('myBox'); // удаление заданного ключа и данных box.delete('key'); // удаление ключа nth и данных box.deleteAt(int index);
В примере мы воспользуемся box.deleteAt()
для более эффективного удаления с помощью конструктора ListView
.
Для чтения, записи и удаления данных существуют и другие инструкции. Но для краткости в статье упомянуты только некоторые из них. За дополнительной информацией можно обратиться к документации Hive.
Простое приложение TODO с базой данных Hive
Лучше всего учиться чему-то новому сразу на практике. Поэтому займемся созданием простого или минимального приложения TODO с использованием базы Hive. Но у вас есть возможность добавлять дополнительные функциональности по своему желанию. Обратите внимание, что мне важнее показать, как работать с базой данных Hive, чем как создать приложение TODO.
Для лучшего понимания обучающий материал сопровождается описаниями в виде комментариев в коде.
Без дальнейших промедлений приступаем к делу.
Предполагается, что вы уже знакомы с Flutter и знаете, как создавать новый проект, добавлять пакеты в файл pubspec.yaml
и т. д.
Добавляем следующие строки в файл проекта pubspec.yaml
:
dependencies:
# Для путей каталога с учетом специфики OS с целью хранения новой созданной базы данных
path_provider: ^1.6.14
hive: ^1.4.3
hive_flutter: ^0.3.0+2
dev_dependencies:
# для создания TypeAdapters
hive_generator: ^0.7.0+2
# для генерации typeAdapters
build_runner: ^1.8.1
Написание кода для приложения Todo
В версии Flutter 1.20 при выполнении асинхронной функции async
внутри главной main
будет выброшена ошибка FlutterError (ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized, т.е. доступ к ServicesBinding.defaultBinaryMessenger был осуществлен до инициализации привязки виджетов). Во избежание этой ошибки необходимо добавить WidgetsFlutterBinding.ensureInitialized()
в качестве первой строки главной функции.
Для инициализации Hive следует указать путь к каталогу, где будут храниться данные. Сделаем это с помощью двух следующих строк:
final applicatonDocumentDir = await path_provider.getApplicationDocumentsDirectory(); Hive.init(applicatonDocumentDir.path);
Главная функция принимает такой вид:
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_db_example/my_home_page.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final applicatonDocumentDir =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(applicatonDocumentDir.path);
runApp(MyApp());
}
Как показано ниже, класс MyApp
не претерпел существенных изменений по сравнению с предварительно созданным классом. Поменялось только название с Flutter Demo
на Simple TODO App Using Hive
.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Simple TODO App Using Hive',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
Для простоты примера создадим вводный класс Task
только с одним атрибутом task
в другом файле task_model.dart
.
class Task{ String task; Task({this.task}); }
Hive поддерживает следующие типы данных:
int
;string
;list
;map
;DateTime
.
Класс Task
имеет только один атрибут, поэтому мы гарантированно можем хранить задачи непосредственно в базе данных, учитывая поддержку типа String
. Однако Task
предполагает возможность добавления и большего числа атрибутов.
У нас не получится напрямую сохранять задачи в базе данных, используя Hive.add(Task(task:’ goto gym’))
. Для класса Task
требуется создать TypeAdapter
. Есть еще один способ преобразования объекта в строку JSON, но, по моему мнению, для этой цели вариант с TypeAdapter
подходит лучше всего.
Сначала создадим TypeAdapter
путем автоматической генерации его кода вместо написания вручную. Так мы избежим многих ошибок и сэкономим время, что особенно целесообразно при работе со сложными классами.
Для автогенерации TypeAdapter
необходим пакет Hive_generator
, который уже был добавлен в pubspec.yaml
. Теперь дописываем строки в класс Task
:
import 'package:hive/hive.dart';
part 'task_model.g.dart';
@HiveType(typeId: 0)
class Task {
@HiveField(0)
String task;
// Просто пример для следующего атрибута
// @HiveField(1)
// bool проверен;
Task({this.task});
}
Следует запомнить один нюанс. Мы пишем точно такое же имя файла модели с ключевым словом part
и затем добавляем к нему расширение .g.dart
. Я уже создала отдельный файл task_model.dart
, поэтому здесь написала task_model.g.dart
.
На этом работа с классом Task
закончена. Теперь начинается магия. Выполняем следующую команду в терминале внутри каталога проекта:
flutter packages pub run build_runner build
Она автоматически сгенерирует новый файл с заданным именем с ключевым словом part
. В данном случае он называется task_model.g.dart
и выглядит так:
// СГЕНЕРИРОВАННЫЙ КОД - НЕ ВНОСИТЕ ИЗМЕНЕНИЙ ВРУЧНУЮ
part of 'task_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class TaskAdapter extends TypeAdapter<Task> {
@override
final int typeId = 0;
@override
Task read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Task(
task: fields[0] as String,
);
}
@override
void write(BinaryWriter writer, Task obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.task);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TaskAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
Созданием TypeAdapter
мы не ограничимся: необходимо его зарегистрировать путем добавления Hive.registerAdapter(TaskAdapter())
в функцию main
после инициализации Hive. Кроме того, я также создала/открыла бокс TODOs
для внесения в него задач. Функция main
выглядит так:
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_db_example/my_home_page.dart';
import 'package:hive_db_example/task_model.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final applicatonDocumentDir =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(applicatonDocumentDir.path);
Hive.registerAdapter(TaskAdapter());
await Hive.openBox<Task>('TODOs');
runApp(MyApp());
}
Проектируем и создаем внешний вид домашней страницы:
Как показано выше, я создала виджет Stateful
с именем домашней страницы, и он возвращает виджет Scaffold
. В Scaffold
я добавила панель приложения с названием Simple TODO app using Hive
, а в тело — виджет ValueListenableBuilder
.
Если ограничиться только Listview.Builder
, то при каждом добавлении новой записи в базу данных придется вручную перестраивать UI, что совсем нежелательно. По этой причине был задействован виджет ValueListenableBuilder
.
В ряде устаревших руководств по Hive упоминается использование виджета WatchBoxBuilder
, который уже в последней версии Hive 1.4.3 утратил свою актуальность. Этим объясняется выбор ValueListenableBuilder
.
Я рекомендую виджет ValueListenableBuilder
из-за его повышенной эффективности. Ведь он перестраивает UI только при обновлении переменной valueListenable
, а в нашем случае при каждом добавлении новой записи о задаче в базу данных.
Ошибка, полученная при выполнении Hive.box<Task>(‘TODOs’).listenable()
, означает, что вы забыли импортировать package:hive_flutter/hive_flutter.dart
в файл myHomePage
. Смотрим код:
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_db_example/task_model.dart';
import 'package:hive_flutter/hive_flutter.dart';
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String inputTask;
Task _task;
Box<Task> todosBox;
@override
Widget build(BuildContext context) {
// notesBox.clear();
return Scaffold(
appBar: AppBar(
title: Text('Simple TODO app using Hive'),
),
body: ValueListenableBuilder(
valueListenable: Hive.box<Task>('TODOs').listenable(),
builder: (context, Box<Task> _notesBox, _) {
todosBox = _notesBox;
return ListView.builder(
itemCount: _notesBox.values.length,
itemBuilder: (BuildContext context, int index) {
final todo = todosBox.getAt(index);
return ListTile(
title: Text(todo.task),
onLongPress: () => todosBox.deleteAt(index),
);
});
}),
floatingActionButton: FloatingActionButton(
onPressed: () => _simpleDialog(),
tooltip: 'AddNewTODOTask',
child: Icon(Icons.add),
),
);
}
}
Как вы заметили, длительным нажатием на ListTile
мы удаляем задачу из списка дел (с помощью todos.deleteAt()
). Кроме того, был добавлен виджет FloatingActionButton
, который открывает простое диалоговое окно для записи рабочих задач. Выглядит он так:
Остановимся на проектировании диалогового окна. Код simpleDialog
простой. Я добавила виджет SimpleDialog
, в который внесла один виджет TextField
и два FlatButton
.
При нажатии кнопки Add
вызывается функция _addTodo
для добавления новой задачи в список дел. Нажимая на кнопку Cancel
, мы возвращаемся на домашний экран. Рассмотрим код для _simpleDialog
и функции _addTodo
(используется todosBox.add()
для внесения задачи в бокс TODOs
):
_simpleDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('New TODO Task'),
children: <Widget>[
Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextField(
decoration: InputDecoration(
hintText: 'TODO Task',
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
),
),
border: InputBorder.none,
),
onChanged: (value) => inputTask = value,
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FlatButton(
color: Colors.blue,
onPressed: () {
_task = Task(task: inputTask);
_addTodo(_task);
Navigator.pop(context);
},
child: Text('Add'),
),
FlatButton(
color: Colors.blue,
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
)
],
),
),
],
),
),
)
],
);
},
);
}
Мы написали код для большей части приложения. Теперь проведем очистку. Hive относится к хранилищам данных, основанным на добавлении записей. Она записывает данные в конец файла бокса, что приводит к его разрастанию. Для решения этой проблемы мы можем вручную применить метод .compact()
или доверить Hive сделать это автоматически.
Я переопределила метод dispose
для закрытия Openbox
. Для указания конкретного бокса использовался todosBox.close()
. Однако также возможен Hive.close()
, который закрывает все открытые боксы перед закрытием страницы.
@override void dispose() { // очищаем удаленные индексные отверстия/слоты из базы данных // освобождаем место todosBox.compact(); todosBox.close(); super.dispose(); }
Добавляем любые задачи и перезапускаем приложение. Оно автоматически откроет все из них на домашнем экране.
Полный вариант кода находится здесь.
Практические задания повышенной сложности
- Добавьте дополнительные функциональности: кнопку
Checkbox
(флажок), тему и оформление приложения. - Попробуйте реализовать приложение для ведения записей.
Заключение
Hive — отличная, простая, быстрая и эффективная база данных. Она предоставляет пользовательские TypeAdapter
, которые были рассмотрены в статье. Особых слов благодарности достоин автор этого изумительного пакета Simon Leier.
Читайте также:
- Введение в библиотеку Flutter Bloc
- Flutter зовет: 5 проектов за выходные
- 5 секретов создания востребованной технической статьи
Читайте нас в Telegram, VK и Дзен
Перевод статьи Radhika patel: Build a Simple To-Do Flutter App Using Hive Database