Как создать простое Flutter-приложение ToDo с помощью Hive

“А не добавить ли в приложение локальную базу данных?”  —  подумала я и начала искать подходящую библиотеку для реализации. И тут мне попадается библиотека 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.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Radhika patel: Build a Simple To-Do Flutter App Using Hive Database

Предыдущая статьяВведение в линейное программирование на Python 
Следующая статья9 советов по работе с консолью JavaScript, которые помогут оптимизировать отладку