В трёх предыдущих параграфах мы рассмотрели важные библиотеки для работы с данными: файлами, парами ключ — значение и SQL/NoSQL БД.
Это — заключительный параграф серии. В нём мы рассмотрим важные нюансы:
-
Как правильно хранить конфиденциальные данные.
-
Как организовать многопоточность при работе с БД.
-
Как выбрать систему для хранения данных.
Хранение секретов
Хранение конфиденциальных данных, таких как API-ключи, требует особой осторожности. Вот несколько распространённых методов для безопасного хранения секретов в приложении:
-
Локальный файл с ключами. Поместите секреты в файл, который исключён из системы контроля версий (например, в файл
.gitignore
). Этот метод обеспечивает базовую безопасность, но не идеальную. -
Файлы окружения
.env
. Используйте файлы окружения для хранения секретных значений. Но и его также нужно не забыть исключить из системы контроля версий, добавив в файл.gitignore
. Чтобы проще взаимодействовать с файлом, можно воспользоваться пакетомenvied
, который в дополнение ко всему «из коробки» предлагает обфусцировать значения. -
Обфускация. Этот метод заключается в том, чтобы усложнить чтение и понимание вашего кода секретного ключа. В этом может помочь указанный выше
envied
. Однако это не обеспечивает полной безопасности, так как декомпиляция приложения может деобфусцировать значения. -
Передача через
--dart-define
. Вы можете передавать секретные значения в приложение на этапе компиляции. Это позволяет избежать хранения секретов в коде приложения и передавать их динамически. Например, с помощью командыflutter run --dart-define SECRET_KEY_1=a1b2t5b6D --dart-define SECRET_KEY_2=b2l4p5fg1
. Если список секретов слишком большой, то начиная с версии Flutter 3.7 используется флаг--dart-define-from-file
и появляется возможность хранить все секреты в файле в формате json.
Пример команды: flutter run --dart-define-from-file=my_secrets.json
.
Применение правильных методов для хранения и управления секретами — важный аспект обеспечения безопасности приложений. Внимательно выбирайте метод, который соответствует вашим требованиям безопасности. Но стоит отметить всё же, что наиболее предпочтительным вариантом остаётся использование --dart-define
.
Мультипоточность при работе с БД
Иногда возникает необходимость записывать/считывать данные из разных изолятов. Например, для ускорения работы приложения создан изолят, который обрабатывает какой-то файл и проводит с ним манипуляции. А в это же самое время основной изолят запрашивает информацию об этом файле.
Вот как это может выглядеть:
Напомним, что так сделать нельзя из веб-платформы.
В тех случаях, когда механизм работы с несколькими изолятами не предусмотрен в самой библиотеке, можно придумать различные способы решения проблемы или обойтись способами, представленными ниже.
Для потокобезопасной работы (isolate-safe) с файлом из разных изолятов следует обеспечить синхронизацию и корректное управление доступом. Например, могут встречаться ситуации, когда один изолят пишет в файл, а другой должен из него читать. Dart и Flutter не предоставляют встроенные механизмы синхронизации, которые используются для управления доступом к общим ресурсам в многопоточных программах (Mutex/Semaphore).
Однако их можно создать самостоятельно.
Создайте централизованный менеджер (обычно в основном изоляте), который будет заниматься всеми операциями и синхронизацией с файлами. Все другие изоляты должны отправлять сообщения к этому менеджеру для выполнения операций чтения или записи. Это обеспечит последовательное выполнение всех операций с данными из одного источника, что предотвратит возможные конфликты.
Используйте lock-файл. Один изолят пишет в файл, другой читает из этого файла. Чтобы избежать конфликтов, можно использовать механизмы типа «лока». Например, можно создать дополнительный файл, действующий как флаг (например, lock.file), который будет создаваться перед началом операции записи и удаляться после её завершения. Изолят, предназначенный для чтения, должен проверять наличие этого файла перед началом чтения.
Пример:
1import 'dart:isolate';
2import 'package:flutter/services.dart';
3import 'dart:io';
4import 'package:path_provider/path_provider.dart';
5
6void main() async {
7 RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
8
9 await Isolate.spawn(
10 _writeFile,
11 _IsolateData(
12 message: "Hello from Isolate 1\n",
13 token: rootIsolateToken,
14 ),
15 );
16 await Isolate.spawn(
17 _writeFile,
18 _IsolateData(
19 message: "Hello from Isolate 2\n",
20 token: rootIsolateToken,
21 ),
22 );
23 // Даём время для завершения записи
24 await Future.delayed(const Duration(seconds: 1));
25 await _readFile(rootIsolateToken);
26}
27
28Future<void> writeFile(IsolateData data) async {
29 BackgroundIsolateBinaryMessenger.ensureInitialized(data.token);
30 final documentsDirectory = await getApplicationDocumentsDirectory();
31 var lockFile = File('${documentsDirectory.path}/file.lock');
32 while (lockFile.existsSync()) {
33 // Небольшая задержка
34 await Future.delayed(const Duration(milliseconds: 50));
35 }
36 await lockFile.create();
37 var file = File('${documentsDirectory.path}/shared_file.txt');
38 await file.writeAsString(data.message, mode: FileMode.append);
39 await lockFile.delete();
40}
41
42Future<void> readFile(RootIsolateToken token) async {
43 BackgroundIsolateBinaryMessenger.ensureInitialized(token);
44 final documentsDirectory = await getApplicationDocumentsDirectory();
45 var file = File('${documentsDirectory.path}/shared_file.txt');
46 if (await file.exists()) {
47 var content = await file.readAsString();
48 print(content);
49 }
50}
51
52class IsolateData {
53 final RootIsolateToken token;
54 final String message;
55
56 IsolateData({
57 required this.token,
58 required this.message,
59 });
60}
Использование библиотек, которые поддерживают транзакции и/или блокировки «из коробки». Пройдёмся по основным библиотекам и посмотрим, какие из них могут предложить функционал работы с мультиизолятами.
В Hive
нет полноценной поддержки работы mutli-isolate с БД. Так что лучше отказаться от этого плагина, если есть необходимость в мультипоточности, или использовать централизованный менеджер
(см. выше).
В Isar
поддержка multi-isolate доступна «из коробки» и описана в документации к пакету. Именно в этом случае и будет использоваться транзакция записи.
Хотя SQFLite
и выполняет операции с базой данных в фоновом потоке (что технически изолирует их от потока пользовательского интерфейса), все обращения к базе данных должны исходить из главного изолята. Механизмы транзакции в SQFLite не поддерживают многопоточный доступ или доступ из разных изолятов. Подробнее про это можно почитать в комментарии от разработчика пакета.
В Drift
можно использовать статические методы NativeDatabase.createInBackground
или NativeDatabase.createBackgroundConnection
(разные способы создания БД в фоновом изоляте), и тогда создастся изолят, в котором будут исполняться SQL-запросы. Это можно сделать без дополнительных настроек. Если всё же требуется работать с БД из разных изолятов, то глобально есть два варианта.
Самый простой — использовать метод computeWithDatabase
(аналогия с compute
) или serializableConnection
для ручной инициализации изолята.
1import 'package:drift/isolate.dart';
2
3...
4
5Future<void> insertSomeData(AppDatabase database) async {
6 await database.computeWithDatabase(
7 computation: (database) async {
8 // Затратная операция выполняется в отдельном изоляте, но взаимодействует с основной базой данных.
9 await database.batch((batch) {
10 batch.insertAll(database.dogs, []);
11 });
12 },
13 connect: (connection) {
14 // Эта функция отвечает за создание второго экземпляра вашего класса базы данных с краткосрочным [connection].
15 // Для работы необходим конструктор класса базы данных, принимающий connection.
16 return AppDatabase(connection);
17 },
18 );
Варианты посложнее с полным контролем жизненного цикла и с кастомизацией поведения используют сущность DriftIsolate
, которая помогает запускать Drift в фоновом изоляте и является ссылкой на внутренний сервер базы данных (RunningDriftServer
), к которому вы можете получить доступ из других изолятов.
Иными словами DriftIsolate
— это единственный изолят, внутри которого исполняются все запросы и обновляются данные, а все остальные изоляты лишь обращаются к нему. Для более продвинутых примеров и дополнительной информации советуем ознакомиться с официальной документацией.
Учитывая всё вышеизложенное, пакет Drift — отличное решение для использования в мультипоточной среде.
Также стоит учитывать, что работа с данными в изоляте помогает добиться лучшего перформанса по UI, но скорость работы в некоторых случаях может быть медленнее, чем без изолятов, так как результат выполнения операций не доступен напрямую, а копируется из изолята, работающего с БД. Однако благодаря недавним улучшениям, таким как isolated groups в Dart VM, эти накладные расходы довольно малы, и там, где это возможно, рекомендуется использовать изоляты для выполнения запросов.
Как выбрать систему для хранения данных
Выбор системы хранения данных — важное решение при разработке приложения на Flutter. Не существует универсального подхода, который удовлетворял бы всем случаям. Поэтому перед тем как выбрать определенное решение, стоит обратить внимание на следующие моменты:
1. Покрытие тестами
Исследуйте, насколько хорошо решение поддерживается тестами. Хорошее покрытие тестами обеспечивает большую уверенность в стабильности и надёжности системы хранения данных.
2. Шифрование
Обращайте внимание на поддержку шифрования. Если это чувствительная информация, то используйте соответствующие библиотеки, не забывая, например, про возникающий трейд-офф в виде затрат на шифрование/расшифрование данных.
3. Популярность
Насколько стандартно и широко используется выбранное решение? Стандартизированные и популярные инструменты обычно имеют более широкую поддержку сообщества и документацию.
4. Реляционность и запросы
Если вам нужно хранить связанные данные и выполнять сложные запросы, рассмотрите реляционные базы данных. Для неструктурированных данных и простых операций выберите NoSQL-решения.
5. Транзакции
Если ваше приложение зависит от транзакционной безопасности (атомарности), убедитесь, что выбранное решение поддерживает эту функцию. Актуально для баз данных.
6. Документация
Исследуйте качество и актуальность документации для выбранного решения. Хорошая документация поможет вам быстро освоить систему и решить возможные проблемы.
7. Использование оперативной памяти
Подумайте о том, как решение управляет памятью. Не все базы данных загружают данные в оперативную память сразу, и это может повлиять на производительность при работе с большими объёмами данных.
8. Миграции
Если формат ваших данных будет меняться в будущем, убедитесь, что выбранное решение поддерживает механизмы миграции данных.
9. Изоляты
Рассмотрите, как выбранное решение взаимодействует с изолятами. Некоторые решения могут быть не приспособлены для работы с изолятами.
Для удобства мы подготовили сводную таблицу, в которой сравнили все решения из трёх предыдущих параграфов:
Шифрование |
Покрытие тестами |
Популярность |
Транзакции |
Документация |
Использование оперативной памяти |
Изоляты |
Миграции |
|
path_provider |
- |
+ |
+ |
- |
+ |
- |
- |
- |
shared_preferences |
- |
+ |
+ |
- |
+ |
+ |
- |
- |
Hive |
+ |
+ |
+- |
+ |
+ |
+ |
+ |
+- |
Isar |
- |
+ |
+- |
+ |
+ |
+ |
+ |
+- |
SQFLite |
+- |
+ |
+ |
+ |
+ |
- |
+- |
+ |
Drift |
+ |
+ |
+- |
+ |
+ |
- |
+ |
+ |
flutter_cache_manager |
- |
+ |
+- |
- |
+ |
- |
- |
- |
flutter_secure_storage |
+ |
+ |
+ |
- |
+ |
- |
- |
- |
get_storage |
- |
+ |
+- |
- |
+ |
+ |
- |
- |
В конечном итоге правильный выбор системы хранения данных будет зависеть от конкретных потребностей вашего проекта и вашего опыта. Помните, что в некоторых случаях лучшим решением может быть комбинация нескольких систем хранения данных, чтобы достичь оптимальной производительности и надёжности.
На этом всё, большую тему рассмотрели. Советуем закрепить знания, выполнив квиз ниже. И переходите к следующему параграфу! В нём мы разберём работу с библиотеками для получения данных по HTTP.