Рано или поздно любой разработчик сталкивается с необходимостью организации локального хранилища данных. При этом вам нужно решить, какие данные хранить, как подготовить их структуру и как обеспечить быстрый и надёжный доступ к этой информации. Мы рассмотрим различные системы хранения данных для приложений Flutter и поможем вам определиться с выбором подходящей для вашего проекта.
Во Flutter существуют различные подходы к решению этого вопроса, и у каждого из них есть свои плюсы и минусы. Мы разберём популярные библиотеки и базы данных, а также детали, на которые нужно обращать внимание при выборе наиболее подходящего варианта для ваших задач.
Это большая тема, поэтому мы разделили её на четыре параграфа.
-
В первом (вы его читатете) мы поговорим о хранении файлов с помощью библиотек
path_provider
иflutter_cache_manager
. -
Во втором — о хранилищах «ключ-значение».
-
В третьем рассмотрим хранение данных в базах SQL и NoSQL.
-
А в четвёртом сфокусируемся на важных нюансах: как хранить конфиденциальные данные и организовать мультипоточность при работе с БД.
Давайте приступим!
Библиотека path_provider
Она нужна для получения корректных путей файлов и директорий на устройстве. Каждая операционная система может иметь разные подходы к сохранению данных, а path_provider
помогает нивелировать эти различия.
Вы уже могли видеть пример использования библиотеки path_provider
ранее. Сейчас мы разберём её возможности и методы подробнее.
Важно: эта библиотека не поддерживает веб. Пример решения под веб мы привели в конце текущей главки.
Методы библиотеки path_provider для поиска директорий
Flutter-приложения могут взаимодействовать с различными директориями для хранения данных. Вот основные методы для поиска директорий:
-
getApplicationDocumentsDirectory()
. Это системная приватная директория приложения, которая создаётся при установке приложения и доступна только для него. Используется для хранения данных, созданных пользователем. -
getApplicationSupportDirectory()
. Эта директория предназначена для хранения файлов, не связанных с пользователем. Например, она может использоваться для хранения данных, которые не находятся в ресурсах приложения. Это могут быть различные конфигурационные файлы и иные данные, необходимые для внутренних операций приложения, а не для пользователя. -
getTemporaryDirectory()
. Эта директория предназначена для хранения временных файлов и кэша. Самая очевидная область применения — это хранение изображений. ОС по своему усмотрению может удалить содержимое этой директории, начиная с самых старых файлов. -
getLibraryDirectory()
. Эта директория предназначена для сохранения баз данных и доступна только на платформе iOS. Именно поэтому перед вызовом метода необходимо обязательно проверять, на какой платформе запущено приложение. -
getExternalStorageDirectory()
иgetExternalCacheDirectories()
. Эти директории предназначены для оптимизации хранения больших данных и доступны только на Android. Android разделяет дисковое пространство на хранилища двух типов: внутреннее и внешнее.getExternalStorageDirectory()
позволяет сохранять большие файлы, например видео в формате 4K, во внешнем хранилище. Здесь также перед вызовом метода необходимо проверять, на какой платформе запущено приложение. -
getDownloadsDirectory()
. Эта директория предназначена для загруженных файлов и доступна только на веб-платформе (web).
Теперь перейдём к манипуляциям непосредственно с файлами, лежащими в директориях.
Класс File
Класс File
из библиотеки dart:io
предоставляет асинхронные методы для взаимодействия с файлами. Вот некоторые из них:
-
delete({bool recursive = false})
. Удаляет файл. Параметрrecursive
указывает, нужно ли удалять файл рекурсивно, если это директория. -
exists
. Проверяет, существует ли файл. -
length()
. Возвращает размер файла. -
readAsBytes()
.Читает файл и возвращает его содержимое в видеUint8List
. -
readAsLines({Encoding encoding = utf8})
. Читает файл и возвращает его содержимое в виде списка строк. -
readAsString({Encoding encoding = utf8})
. Читает файл и возвращает его содержимое в виде строки. -
rename(String newPath)
. Переименовывает файл. -
writeAsBytes(List<int> bytes, {FileMode mode = FileMode.write, bool flush = false})
. Записывает данные в файл в виде байтов. -
writeAsString(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false})
. Записывает данные в файл в виде строки. -
openWrite()
. Возвращает объектIOSink
, который позволяет работать с файлами на более низком уровне и записывать данные порциями. Рекомендуется использовать, если необходимо записать большой поток данных.
Если вам нужны синхронные аналоги указанных функций, то их можно получить, добавив суффикс Sync
. Например, openSync()
, readAsBytesSync()
, renameSync()
. Следует помнить, что при синхронном вызове функций можно заблокировать основной поток исполнения, а значит, целесообразно пользоваться при этом отдельным изолятом.
Вот как выглядит пример записи и чтения данных из файла.
1import 'dart:io';
2import 'package:path_provider/path_provider.dart';
3
4void main() async {
5 final documentsDirectory = await getApplicationDocumentsDirectory();
6 final file = File('${documentsDirectory.path}/somefile.txt');
7
8 // Записываем строку в файл
9 await file.writeAsString('Содержимое файла');
10
11 // Используем IOSink для записи в конец содержимого файла
12 final ioSink = file.openWrite(mode: FileMode.writeOnlyAppend);
13 // Мы также можем записывать содержимое байт из потока
14 // В данном случае это будет "ABC"
15 await ioSink.addStream(Stream.value([65, 66, 67]));
16 // Каждая строка, записанная с помощью writeln(), будет завершаться
17 // переводом указателя на новую строку
18 ioSink.writeln('');
19 ioSink.writeln('The End');
20 // Закрываем экземпляр IOSink и высвобождаем ресурсы
21 await ioSink.close();
22
23 // Читаем файл как строку
24 final content = await file.readAsString();
25 print(content);
26}
Важно: dart:io
также не поддерживает работу в вебе.
Иногда для более эффективного чтения и записи файлов разработчику нужно работать с файлами с использованием стримов. У класса File
есть методы и на такие случаи — рассмотрим их подробнее.
Использование стримов
К примеру, чтение/запись через стримы понадобятся для файлов большого объёма. Без использования подобного механизма весь файл просто будет выгружен в оперативную память. Чтобы избежать этого и прочитать файл построчно, вы можете сделать следующее:
1import 'dart:io';
2
3void main() async {
4 final file = File('somefile.txt');
5 final lines = file.openRead()
6 .transform(utf8.decoder)
7 .transform(LineSplitter());
8
9 await for (var line in lines) {
10 print('$line: ${line.length} символов');
11 }
12}
Запись в файл с помощью стрима:
1import 'dart:io';
2
3var file = File('somefile.txt');
4var sink = file.openWrite();
5sink.write('File accessed ${DateTime.now()}');
6// Закрываем IOSink
7sink.close();
А чтобы обращаться не в начало файла, а прочитать с произвольной позиции, используем следующие функции:
1import 'dartio';
2
3final file = File('somefile.txt');
4// Открытие файла на чтение из произвольной позиции
5final randomAccessFile = await file.open();
6// Перемещаемся в нужную часть файла
7await randomAccessFile.setPosition(100);
8// Читаем первые 50 байт
9final bytes = await randomAccessFile.read(50);
Такой способ также подойдёт при чтении файлов больших объёмов или если мы знаем структуру файла, то есть понимаем, когда нам нужно смещаться на то или иное количество байт.
Таким образом, существует три способа взаимодействия с файлом, и всеми ими можно воспользоваться при отладке приложений как на физическом устройстве, так и на виртуальном. Однако в последнем случае следует учесть некоторые особенности. Расскажем о них подробнее.
Отличия в работе с эмуляторами
Важно отметить разницу в работе с файловой системой между эмулятором Android и симулятором iOS.
Эмулятор Android эмулирует диск и дисковое пространство, поэтому вы можете просматривать файлы, созданные в нём. А iOS-симулятор сохраняет файлы в файловой системе macOS, и вы не сможете увидеть их напрямую в симуляторе.
Чтобы убедиться, переходим в Android Studio → View → Tool Windows → Device File Explorer.

Далее директория data → data → <application ID> → app_flutter.

Работа с вебом
Как мы сказали выше, библиотека path_provider
и dart:io
недоступны на веб-платформе (обсуждение данного вопроса можно посмотреть в репозитории Flutter). Это связано с недоступностью файловой системы из веба по соображениям безопасности.
В таком случае вместо указанных библиотек можно воспользоваться библиотекой file_picker
. То есть после того, как пользователь выберет нужный файл, разработчик получит экземпляры класса PlatformFile
и уже с ними может проводить манипуляции. Пример получения файла в вебе с использованием file_picker
версии 8.1.2:
1final pickedResult = await FilePicker.platform.pickFiles();
2if (pickedResult != null) {
3 print(pickedResult.files.first.name);
4}
С этим разобрались. А теперь подведём промежуточные итоги.
Преимущества и недостатки path_provider
Преимущества |
Недостатки |
|
|
Двигаемся дальше — теперь рассмотрим библиотеку flutter_cache_manager
.
Библиотека flutter_cache_manager
Библиотека flutter_cache_manager
предназначена для кэширования файлов приложения. Это полезный инструмент для управления кэшем и ускорения загрузки файлов.
Например, когда нужно временно локально сохранить часто используемые и просматриваемые файлы/документы, но при этом не бояться их потерять.
Особенности и возможности
-
Поддерживает in-memory хранилище для быстрого доступа к кэшированным файлам.
-
Кэширование происходит в файловой системе устройства.
-
Встроенный механизм управления очередью загрузки.
-
Для загрузки файлов использует библиотеку HTTP.
-
Часто используется вместе с библиотекой
cached_network_image
для управления кэшированием изображений. -
Доступна на всех платформах.
Виды менеджеров кэша
flutter_cache_manager
предоставляет четыре вида менеджеров:
-
BaseCacheManager. Абстрактный класс, который даёт базовую функциональность для управления кэшем.
-
CacheManager. Менеджер кэша с настраиваемой конфигурацией. Позволяет создавать кастомные менеджеры с разными настройками.
-
ImageCacheManager. Миксин, позволяющий работать с кэшированием изображений. Удобен для управления кэшированием изображений в Flutter-приложениях.
-
DefaultCacheManager. Стандартная реализация менеджера кэша с пустой конфигурацией. Предназначен для работы с изображениями.
Основные методы
Ниже мы собрали основные и часто используемые методы библиотеки. С остальными можно ознакомиться в документации.
-
getSingleFile
. Загружает файл по указанному URL или возвращает файл из кэша, если он доступен и срок его хранения не подошёл к концу. -
getFileStream(url)
. ВозвращаетStream<File>
для файла, соответствующего указанному URL. -
downloadFile(url)
. Загружает файл по указанному URL и возвращаетFuture<File>
. -
getFileFromCache
. Получает файл из кэша по URL. -
putFile
. Помещает файл в кэш. -
removeFile
. Удаляет файл из кэша. -
emptyCache
. Очищает весь кэш. -
getImageFile
. Получает файл из кэша, специфично для изображений.
Применение
В самом базовом варианте мы просто сохраняем файл и далее пользуемся им как локальным.
1import 'package:flutter_cache_manager/flutter_cache_manager.dart';
2
3...
4
5// Далее работаем с обычным экземпляром класса File
6final file = await DefaultCacheManager().getSingleFile(
7 'https://yastatic.net/s3/ml-handbook/admin/edu_logo_0640b6dbf8.svg',
8);
9print(file.path);
Преимущества и недостатки flutter_cache_manager
Преимущества |
Недостатки |
|
|
Как видите, flutter_cache_manager
удобно использовать для управления кэшированными файлами в приложении. Это позволяет улучшить производительность приложения и снизить нагрузку на сеть.
А в следующем параграфе мы поговорим о библиотеках, которые помогают работать с хранилищами пар «ключ — значение».