В двух предыдущих параграфах мы рассмотрели библиотеки для работы с файлами и хранилищами пар ключ — значение. В этом — поговорим про библиотеки для работы с базами данных.
Но сперва давайте коротко вспомним, чем различаются реляционные (SQL) и нереляционные (NoSQL) базы данных.
SQL |
NoSQL |
Подходят для хранения структурированных данных. |
Оптимизированы для быстрого доступа к данным. |
Уделяют внимание целостности и надёжности данных. |
Могут обрабатывать данные с нечёткой, неопределённой структурой, подверженной частым изменениям. |
Структура данных обычно не подвержена частым изменениям. |
Например, для кэширования товаров в онлайн-магазине удобнее использовать SQL БД, потому что структура данных включает в себя сложные связи, такие как отношения товара с категорией и другими параметрами. В этом случае SQL может обеспечить лучшую производительность и упростить запросы.
И напротив, приложение, которое предоставляет пользователям данные на основе их местоположения может не иметь чёткой структуры данных и постоянно изменяться. Поэтому NoSQL базы данных позволят разработчикам гибко модифицировать схему без необходимости проведения сложных миграций.
SQL- и NoSQL-решения для Flutter
В экосистеме Flutter есть разнообразные инструменты для работы с базами данных, включая как SQL-, так и NoSQL-решения. Вот некоторые самые популярные из них:
NoSQL |
SQL |
Hive |
SQFLite — надстройка над SQLite, специфичная для каждой платформы. |
Sembast |
Drift (ранее Moor) — использует FFI (foreign function interface — вызов функций языка C) и реализован на Dart. |
ObjectDB |
Floor |
ObjectBox |
|
Isar |
|
Realm |
|
Mongo_dart |
Все рассматривать не будем, рассмотрим только самые популярные:
-
Hive
-
Isar
-
SQFLite
-
Drift
Hive
ПРЕДУПРЕЖДЕНИЕ
ПРЕДУПРЕЖДЕНИЕ
Авторы этой библиотеки могут в любой момент прекратить её поддерживать. Важно принимать во внимание этот факт, несмотря на популярность пакета. Пожалуйста, ознакомьтесь с обсуждением будущего проекта по ссылке, чтобы быть в курсе возможных изменений и перспектив дальнейшей поддержки пакета.
Hive
— это NoSQL-хранилище, предназначенное для сохранения данных в формате «ключ — значение» (key-value). Hive
позволяет вам сохранять не только примитивные данные, но и сложные объекты. Он предоставляет унифицированный способ хранения данных на всех поддерживаемых платформах (mobile/desktop/web).
На момент написания статьи существует предрелизная версия 4.0.0-dev.2 — обёртка над другим пакетом Isar. Однако в главе рассматривается последняя стабильная версия 2.2.3, так как стабильные версии гарантируют, что приложение будет работать предсказуемо, минимизируя возможность возникновения неожиданных сбоев и ошибок.
Центральное понятие в Hive
— это сущность Box
. Она используется для организации и хранения данных. Каждый Box
представляет собой файл, созданный в рабочей директории.
Одно приложение может содержать несколько Box
, и их можно рассматривать как таблицы в реляционных базах данных. При открытии содержимое Box
полностью считывается и загружается в кэш, что обеспечивает быстрый доступ. Строгой структуры данных внутри Box
нет.
Использование Hive
Для начала работы с Hive
выполните следующие действия:
- Импортируйте необходимые зависимости.
1import 'package:hive/hive.dart';
2import 'package:hive_flutter/hive_flutter.dart';
- Инициализируйте
Hive
, указав рабочую директорию в папке приложения при запуске.
1await Hive.initFlutter();
Или же можно проинициализировать вручную, при этом указав любую другую директорию.
1final path = await getApplicationCacheDirectory();
2Hive.init(path.path);
- Откройте или создайте
Box
, где будут храниться данные.Box
могут быть типизированы, что обеспечивает типобезопасность данных.
1final box = await Hive.openBox<User>('users');
- Вы можете считывать, записывать и удалять данные внутри Box.
1await box.put('name', 'Michael');
2final name = box.get('name');
3await box.delete('name');
- При завершения работы с
Box
, как и любой другой ресурс, его следует закрыть (все кэшированные ключи и значенияBox
будут очищены из памяти).
1await box.close();
Теперь файл можно найти в директории приложения.

Работа с кастомными моделями в Hive
Hive
позволяет сохранять кастомные модели с использованием адаптеров. Адаптер определяет, как объекты могут быть сохранены и считаны с диска. Иными словами, адаптер нужен для сериализации/десериализации не примитивных типов данных.
Вот пример создания адаптера для модели User
:
1import 'package:hive/hive.dart';
2
3class UserAdapter extends TypeAdapter<User> {
4 @override
5 int get typeId => 0;
6
7 @override
8 User read(BinaryReader reader) {
9 return User(
10 name: reader.readString(),
11 age: reader.readInt(),
12 );
13 }
14
15 @override
16 void write(BinaryWriter writer, User user) {
17 writer.writeString(user.name);
18 writer.writeInt(user.age);
19 }
20}
21
После создания адаптера его необходимо зарегистрировать:
1Hive.registerAdapter(UserAdapter());
Теперь вы можете сохранять и загружать объекты типа User
в Hive.
Кодогенерация в Hive
Вы также можете использовать кодогенерацию с аннотациями, чтобы проще создавать адаптеры. Для использования генерации нужно добавить зависимость hive_generator: [version]
в секциюdev_dependencies
файла pubspec.yaml
. Вот как это можно сделать для модели User
:
1import 'package:hive/hive.dart';
2
3part 'user.g.dart';
4
5@HiveType(typeId: 0)
6class User {
7 @HiveField(0)
8 final String name;
9
10 @HiveField(1)
11 final int age;
12
13 User({
14 required this.name,
15 required this.age,
16 });
17}
Это позволит значительно упростить процесс сохранения и загрузки данных. Но есть нюанс — с одной стороны, мы не пишем лишние строки кода, но, с другой, на каждый перегон генерации тратится время, плюс могут возникнуть конфликты кода в командной разработке.
Преимущества и недостатки библиотеки hive
Преимущества |
Недостатки |
|
|
Hive
остаётся мощным инструментом для хранения данных во Flutter, обеспечивая простой и удобный способ сохранения данных на стороне клиента. Несмотря на некоторые ограничения, он остается одним из популярных выборов для хранения ключевых данных.
Isar
ПРЕДУПРЕЖДЕНИЕ
ПРЕДУПРЕЖДЕНИЕ
Авторы этой библиотеки могут в любой момент прекратить её поддерживать. Важно принимать во внимание этот факт, несмотря на широкое распространение и популярность пакета. Пожалуйста, ознакомьтесь с обсуждением будущего проекта по ссылке, чтобы быть в курсе возможных изменений и перспектив дальнейшей поддержки пакета.
isar
— это библиотека для работы с базами данных во Flutter. Она также использует кодогенерацию для создания таблиц и обеспечивает множество стандартных удобных возможностей для работы с данными, таких как ACID-транзакции (обеспечивают надёжное и безопасное хранение данных), производительность операций с данными (библиотека написана на Rust).
Начало работы с isar
Перед началом использования isar
вам потребуется добавить несколько библиотек для работы с базами данных:
-
сам пакет
isar
; -
isar_flutter_libs
— нативные библиотеки для различных платформ; -
isar_generator
иbuild_runner
— для генерации кода на основе определений моделей данных.
Вот пример на последней стабильной версии 3.1.0.
1isar_version: &isar_version [version]
2
3dependencies:
4 isar: *isar_version
5 isar_flutter_libs: *isar_version
6
7dev_dependencies:
8 isar_generator: *isar_version
9 build_runner: any
Пример начала работы с isar
, в котором откроем/создадим БД с использованием схемы, описанной дальше в примере:
1final dir = await getApplicationDocumentsDirectory();
2final isar = await Isar.open(
3 [GithubProfileIsarSchema],
4 directory: dir.path,
5);
Модель данных
В isar
вы создаёте модель данных, определяя её поля и их типы. Важно отметить, что все поля в модели всегда указываются как опциональные.
Пример модели GithubProfileIsar
:
1@Collection()
2class GithubProfileIsar {
3 Id id = Isar.autoIncrement;
4 String? login;
5 @Name("avatar_url")
6 String? avatarUrl;
7 String? bio;
8 String? name;
9}
В качестве аннотации можно использовать как Collection, так и collection. Отличие в том, что в Collection можно настраивать дополнительные параметры:
-
inheritance — могут ли свойства и аксессоры наследоваться от родительских классов и миксинов;
-
аксессор-геттер, с помощью которого можно обратиться к коллекции (например, таким образом:
isar.myCustomGithubProfileIsars
); -
ignore — множество свойств и геттеров, которые следует игнорировать.
Далее, чтобы пользоваться коллекциями, мы должны получить схему — формальное описание структуры данных в БД — и запустить генерацию командой dart run build_runner build
. После кодогенерации получаем схему GithubProfileIsarSchema
:
1extension GetGithubProfileIsarCollection on Isar {
2 IsarCollection<GithubProfileIsar> get githubProfileIsars => this.collection();
3}
4
5const GithubProfileIsarSchema = CollectionSchema(
6 name: r'GithubProfileIsar',
7 id: 5589650545759511826,
8 properties: {
9 r'avatar_url': PropertySchema(
10 id: 0,
11 name: r'avatar_url',
12 type: IsarType.string,
13 ),
14 r'bio': PropertySchema(
15 id: 1,
16 name: r'bio',
17 type: IsarType.string,
18 ),
19 r'login': PropertySchema(
20 id: 2,
21 name: r'login',
22 type: IsarType.string,
23 ),
24 r'name': PropertySchema(
25 id: 3,
26 name: r'name',
27 type: IsarType.string,
28 )
29 },
30 estimateSize: _githubProfileIsarEstimateSize,
31 serialize: _githubProfileIsarSerialize,
32 deserialize: _githubProfileIsarDeserialize,
33 deserializeProp: _githubProfileIsarDeserializeProp,
34 idName: r'id',
35 indexes: {},
36 links: {},
37 embeddedSchemas: {},
38 getId: _githubProfileIsarGetId,
39 getLinks: _githubProfileIsarGetLinks,
40 attach: _githubProfileIsarAttach,
41 version: '3.0.0',
42);
43
44int _githubProfileIsarEstimateSize(...) { ... }
45
46void _githubProfileIsarSerialize(...) { ... }
47
48GithubProfileIsar _githubProfileIsarDeserialize(...) { ... }
49
50int _githubProfileIsarEstimateSize(...) { ... }
51
52... и т.д.
Операция записи
Операции записи данных в явном виде выполняются с использованием транзакций (одна или несколько операций с базой данных происходят за единицу работы). Выполняет асинхронные транзакции записи метод writeTxn()
.
1await isar.writeTxn(() {
2 return isar.githubProfileIsars.put(
3 GithubProfileIsar()
4 ..id = 2
5 ..name = 'Tom'
6 ..login = 'tomy00',
7 );
8});
После чего можем снова найти файл с нашей БД в директории приложения.

Операция чтения
В отличие от операции записи, чтение данных выполняется без использования транзакций.
Пример чтения данных:
1final result =
2 await isar.githubProfileIsars.filter().nameContains('T').findFirst();
3 print(result?.id);
4 print(result?.name);
5 print(result?.login);
Вам также доступен инспектор данных для отладки и мониторинга вашей базы данных. Для этого необходимо перейти по ссылке в дебаг - панели после запуска приложения.
Пример работы инспектора из официальной документации пакета:

Преимущества и недостатки isar
Преимущества |
Недостатки |
|
|
Isar
представляет собой неплохое решение для работы с хранением данных, зарекомендовавшее себя и отвоевавшее долю пользователей среди разработчиков. Однако стоит рассмотреть конкретные требования к проекту и всё же поискать альтернативы перед тем, как выбрать этот пакет.
SQFLite
SQFLite
— это плагин для Flutter, который представляет собой обёртку над нативными реализациями SQLite, встраиваемой реляционной базы данных. Он предоставляет мощные средства для хранения и управления данными в приложении, такие как SQL-запросы, транзакции, миграции.
Особенности и возможности
-
Хранение сложно структурированных данных — подходит для реляционных баз данных.
-
Использует платформенные реализации SQLite для Android и iOS.
-
Не поддерживается в веб-версии Flutter.
-
Позволяет выполнять SQL-запросы для выборки и модификации данных.
-
Данные хранятся в одном файле, что облегчает управление.
Инициализация базы данных
Для начала работы с SQFLite
вы должны инициализировать базу данных.
Вот пример инициализации:
1final path = join(await getDatabasesPath(), 'dogs.db');
2final database = await openDatabase(
3 path,
4 onCreate: (db, version) {
5 return db.execute(
6 'CREATE TABLE dogs (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)',
7 );
8 },
9 version: 1,
10);
В этом примере мы открываем или создаём базу данных по указанному пути. Если база уже существует, она просто открывается. Если база не существует, выполняется метод onCreate
, который создаёт таблицу "dogs" с указанными столбцами.
Операции с данными
SQFLite
предоставляет методы для выполнения операций с данными, такие как вставка, выборка, обновление и удаление. Можно выполнять «сырые» запросы в формате SQL.
Вставка данных
1const dog = Dog(
2 name: 'Richy',
3 age: 5,
4);
5int id = await database.insert('dogs', dog.toMap());
Выборка данных
1final dogs = (await database.query('dogs')).map((dog) => Dog.fromMap(dog));
Обновление данных
1int count = await database.update(
2 'dogs',
3 {'age': 6},
4 where: 'id = ?',
5 whereArgs: [1],
6);
Удаление данных
1int count = await database.delete(
2 'dogs',
3 where: 'id = ?',
4 whereArgs: [1],
5);
Миграции и обновление схемы данных
При изменении схемы данных вам следует обновить базу данных с использованием механизма миграции. Вы можете переопределить метод onUpgrade
внутри метода openDatabase
. Это позволит обновлять структуру базы данных при необходимости.
1final path = join(await getDatabasesPath(), 'dogs.db');
2final database = await openDatabase(
3 path,
4 onUpgrade: (Database db, int oldVersion, int newVersion) {
5 if (oldVersion < newVersion) {
6 db.execute("ALTER TABLE dogs ADD COLUMN breed TEXT;");
7 }
8 },
9 version: 2,
10);
Преимущества и недостатки SQFLite
Преимущества |
Недостатки |
|
|
SQFLite
предоставляет доступ к базе данных SQLite, которая известна своей надёжностью и мощностью, а также к эффективному управлению данными, используя возможности SQL.
Drift
Drift
— мощная библиотека для хранения данных (ранее известная как Moor), тоже использующая «под капотом» SQLite. Среди сильных сторон этой библиотеки стоит отметить поддержку всех платформ, миграций, тонких настроек открытия файла БД.
Инициализация базы данных
В процессе создания класса базы данных можно указывать, например, информацию о миграциях. Подробнее с этим можно ознакомиться на официальном сайте. В этой главе мы рассмотрим базовый пример с CRUD-операциями.
Перед началом работы с библиотекой подключим плагин версии 2.20.0 и добавим дополнительный плагин drift_dev
для кодогенерации.
1dependencies:
2 drift: [version]
3
4dev_dependencies:
5 drift_dev: [version]
6 build_runner: any
Далее указываем источник базы данных и создаём инстанс, который будет использоваться для дальнейшего доступа к данным:
1final appDir = await getApplicationDocumentsDirectory();
2final dbPath = '${appDir.path}/dogs.db';
3final dbFile = File(dbPath);
4
5final dbConnection = NativeDatabase.createBackgroundConnection(dbFile);
6final db = AppDatabase(dbConnection);
И создаём объект базы данных с аннотацией @DriftDatabase:
1@DriftDatabase(tables: [Dogs, Breeds])
2class AppDatabase extends _$AppDatabase {
3 AppDatabase(QueryExecutor e) : super(e);
4
5 @override
6 int get schemaVersion => 1;
7
8 // Метод получения всех записей
9 Future<List<Dog>> getAllDogs() async {
10 return await select(dogs).get();
11 }
12
13 // Сохраняем запись про собаку
14 Future<int> saveDog(DogsCompanion companion) async {
15 return await into(dogs).insert(companion);
16 }
17
18 // Сохраняем запись про породу собаки
19 Future<int> saveBreed(BreedsCompanion companion) async {
20 return await into(breeds).insert(companion);
21 }
22
23 // Получаем данные о породе по идентификатору
24 Future<Breed?> getBreedById(int? id) async {
25 if (id == null) {
26 return null;
27 }
28
29 final value = select(breeds)..where((breed) => breeds.id.equals(id));
30 return value.getSingleOrNull();
31 }
32
33 // Удаляем все записи. При этом идентификаторы не «сбрасываются»
34 // и продолжают инкрементироваться.
35 Future<void> removeAll() async {
36 return transaction(() async {
37 await delete(dogs).go();
38 await delete(breeds).go();
39 });
40 }
41
42 // Обновляем запись
43 Future<void> updateDog(Dog updatedDog) async {
44 update(dogs).replace(updatedDog);
45 }
46
47 // Переопределяем в случае переезда базы на новую структуру данных
48 @override
49 MigrationStrategy get migration => super.migration;
50}
Наконец определяем модели данных:
1@DataClassName('Dog')
2class Dogs extends Table {
3 IntColumn get id => integer().autoIncrement()();
4
5 TextColumn get name => text()();
6
7 // Храним id породы собаки
8 IntColumn get breed => integer().nullable().references(Breeds, #id)();
9
10 DateTimeColumn get birthday => dateTime().nullable()();
11}
12
13@DataClassName('Breed')
14class Breeds extends Table {
15 IntColumn get id => integer().autoIncrement()();
16
17 TextColumn get name => text()();
18}
И запускаем кодогенерацию flutter pub run build_runner build
.
Операции с данными
Далее будем использовать язык dart для построения базовых запросов. Мы пользуемся методами, определёнными в классе AppDatabase
.
Сохранение данных
Для сохранения данных можно выполнить следующие операции:
1const dogModelOne = DogsCompanion(
2 name: Value('Fluffy Fluff'),
3);
4db.saveDog(dogModelOne);
5
6const breedOne = BreedsCompanion(
7 name: Value('Sheepdog'),
8);
9final breedId = await db.saveBreed(breedOne);
10
11final dogModelTwo = DogsCompanion(
12 name: const Value('Snoppy'),
13 breed: Value(breedId),
14);
15db.saveDog(dogModelTwo);
Или же:
1db.into(db.dogs).insert(
2 const DogsCompanion(
3 name: Value('Fluffy Fluff'),
4 ),
5 );
В более продвинутых сценариях для подтверждения сохранности изменения данных могут пригодиться транзакции. Самостоятельно ознакомиться с этим механизмом можно в документации.
Выборка данных
1final dogs = await db.getAllDogs();
2print(dogs);
Обновление данных
1final updateDogModelOne = dogs.first.copyWith(
2 name: 'Fluffy',
3);
4db.updateDog(updateDogModelOne);
Удаление данных
1await db.removeAll();
Преимущества и недостатки Drift
Преимущества |
Недостатки |
|
|
Данную библиотеку стоит выбирать при создании сложных приложений с нетривиальными системами хранения данных, поскольку плагин покрывает весь необходимый функционал большинства приложений.
На данный момент это одно из лучших решений для хранения в БД. Однако стоит учитывать свои потребности и, если обстоятельства того не требуют, выбрать более простое решение.
Теперь мы готовы перейти к заключительной части нашего рассказа про системы хранения данных. В ней мы рассмотрим разные нюансы хранения данных и определимся, как выбрать наиболее подходящее решение под ваши задачи.