4.1. Системы хранения данных: библиотеки для работы с файлами

Рано или поздно любой разработчик сталкивается с необходимостью организации локального хранилища данных. При этом вам нужно решить, какие данные хранить, как подготовить их структуру и как обеспечить быстрый и надёжный доступ к этой информации. Мы рассмотрим различные системы хранения данных для приложений 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.

4.1.1.webp

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

4.1.2.webp

Работа с вебом

Как мы сказали выше, библиотека 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

Преимущества

Недостатки

  • Поддержка многих популярных платформ
    (Windows/MacOS/Linux/iOS/Android).

  • Простота в использовании.

  • Различные варианты работы с файлами
    (синхронно/асинхронно).

  • Из-за многообразия платформ
    и их различий приходится ставить условия на проверку текущей платформы.

  • Нет поддержки веба.

Двигаемся дальше — теперь рассмотрим библиотеку flutter_cache_manager.

Библиотека flutter_cache_manager

Библиотека flutter_cache_manager предназначена для кэширования файлов приложения. Это полезный инструмент для управления кэшем и ускорения загрузки файлов.

Например, когда нужно временно локально сохранить часто используемые и просматриваемые файлы/документы, но при этом не бояться их потерять.

Особенности и возможности

  • Поддерживает in-memory хранилище для быстрого доступа к кэшированным файлам.

  • Кэширование происходит в файловой системе устройства.

  • Встроенный механизм управления очередью загрузки.

  • Для загрузки файлов использует библиотеку HTTP.

  • Часто используется вместе с библиотекой cached_network_image для управления кэшированием изображений.

  • Доступна на всех платформах.

Виды менеджеров кэша

flutter_cache_manager предоставляет четыре вида менеджеров:

  1. BaseCacheManager. Абстрактный класс, который даёт базовую функциональность для управления кэшем.

  2. CacheManager. Менеджер кэша с настраиваемой конфигурацией. Позволяет создавать кастомные менеджеры с разными настройками.

  3. ImageCacheManager. Миксин, позволяющий работать с кэшированием изображений. Удобен для управления кэшированием изображений в Flutter-приложениях.

  4. 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

Преимущества

Недостатки

  • Простой и интуитивно понятный API.

  • Управление кэшированием файлов через разные виды менеджеров.

  • Очень условный минус, но лишнее затягивание зависимостей.
    Вместо сторонней зависимости в некоторых сценариях
    можно обойтись собственной реализацией на path_provider и getTemporaryDirectory().

Как видите, flutter_cache_manager удобно использовать для управления кэшированными файлами в приложении. Это позволяет улучшить производительность приложения и снизить нагрузку на сеть.

А в следующем параграфе мы поговорим о библиотеках, которые помогают работать с хранилищами пар «ключ — значение».

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Предыдущий параграф3.17. Доступность
Следующий параграф4.2. Системы хранения данных: библиотеки для работы с парами ключ — значение