Перейти к содержанию

Storage

Способ хранения зависит от вида данных и того, кому они нужны: Room подходит для связанных записей, DataStore - для небольших настроек, каталоги приложения - для его собственных файлов, общее хранилище - для медиафайлов и документов пользователя. Сохранение данных между запусками отличается от временного состояния экрана.

SQLite и Room

SQLiteOpenHelper

Шпаргалка по SQLite

SQLiteOpenHelper управляет созданием, открытием и обновлением версии базы при прямой работе с SQLiteDatabase. Класс-наследник задаёт имя и версию базы и реализует onCreate() и onUpgrade(). Создание объекта помощника ещё не открывает базу: это происходит при первом вызове getReadableDatabase() или getWritableDatabase().

Для новых реляционных баз обычно выбирают Room. Он тоже использует SQLite, но проверяет поддерживаемые SQL-запросы при компиляции и предоставляет DAO и механизм миграций. Прямая работа с SQLite остаётся актуальной в существующем коде и там, где нужен низкоуровневый контроль.

onCreate() / onUpgrade()

onCreate() инициализирует новую базу, обычно создавая таблицы и индексы. onUpgrade() вызывается, если версия существующей базы ниже запрошенной версии. При понижении версии стандартная реализация выбрасывает исключение, если не переопределить onDowngrade().

Продумайте переход с каждой поддерживаемой старой версии: пользователь может пропустить несколько выпусков приложения. Сохраняйте данные, если только их потеря явно не допустима; удаление и повторное создание пользовательских таблиц уничтожит их содержимое. Проверяйте обновление на схемах прошлых выпусков. Изменение кода уже выполненной миграции не запустит её повторно на обновлённых устройствах. У Room собственные механизмы явных и автоматических миграций; их не следует смешивать с SQLiteOpenHelper.onUpgrade().

getReadableDatabase() / getWritableDatabase()

getWritableDatabase() открывает базу для чтения и записи либо выбрасывает исключение, если это невозможно. getReadableDatabase() обычно возвращает такую же базу, но при проблеме, например переполненном диске, может открыть её только для чтения. Не предполагайте, что результат всегда доступен для записи.

Открытие может включать создание или миграцию базы и блокировать поток, поэтому вызывайте эти методы вне главного потока. Повторно используйте помощник в пределах подходящего времени жизни, а не открывайте и закрывайте его для каждого запроса; закройте его, когда он больше не нужен. Объединяйте связанные операции записи в транзакцию для сохранения согласованности.

Room

В Room @Entity описывает таблицу, @Dao - операции с данными, а @Database - точку доступа к базе. Он подходит для офлайн-записей, кэша, истории и других структурированных данных со связями и запросами. Продумывать индексы и миграции всё равно необходимо.

По умолчанию Room запрещает доступ к базе в главном потоке. Используйте suspend-функции DAO для отдельных операций и Flow для наблюдаемых запросов; Room выполняет такие асинхронные запросы вне главного потока. Для нескольких связанных операций записи используйте транзакцию. Не выбирайте миграцию с удалением данных, которые пользователь ожидает сохранить.

Настройки

DataStore

DataStore хранит небольшие постоянные значения и работает с корутинами и Flow. Preferences DataStore использует ключи без фиксированной схемы; Proto DataStore хранит типизированные сообщения, описанные в Protocol Buffers. Чтение предоставляет Flow; для атомарных обновлений Preferences DataStore использует edit(), а Proto DataStore - updateData().

Он подходит для настроек и небольших фрагментов состояния приложения: например, темы или признака завершения знакомства с приложением. Для связанных данных, больших наборов и запросов к отдельным записям используйте Room. В одном процессе создавайте один экземпляр DataStore на файл; если к файлу обращаются несколько процессов, последовательно используйте многопроцессный вариант. Сам по себе DataStore не шифрует содержимое.

SharedPreferences

SharedPreferences хранит небольшой набор настроек в виде пар «ключ - значение» и часто встречается в старом Android-коде. apply() сразу обновляет данные в памяти и планирует запись на диск, не сообщая об ошибке. commit() записывает данные синхронно и возвращает признак успеха. Не вызывайте commit() в главном потоке; ожидающие записи apply() тоже могут задержать переходы жизненного цикла и привести к ANR.

Для новых настроек предпочитайте DataStore. При переносе существующих настроек используйте его механизм миграции и проверьте значения по умолчанию и соответствие ключей. Ни обычный SharedPreferences, ни DataStore сами по себе не обеспечивают безопасное хранение секретов: для конфиденциальных данных нужна подходящая защищённая схема.

Файлы и общее хранилище

Файлы, нужные только приложению, сохраняйте в его собственных каталогах. filesDir предназначен для постоянных приватных файлов, cacheDir - для временного кэша; система может удалять файлы кэша, а файлы приложения обычно удаляются при его удалении. Для крупных собственных файлов доступно и внешнее хранилище приложения, но соответствующий том может быть недоступен.

Для публикации фото и видео вне приложения используйте MediaStore; для выбора пользователем уже существующих фото и видео - системный выбор фотографий. Для документов, выбираемых или создаваемых пользователем, используйте Storage Access Framework. Возвращённый URI вида content:// указывает на содержимое: не считайте его путём к файлу. Правила доступа и разрешения зависят от операции и версии Android.

Выбор API подробнее описан в руководстве по хранению данных Android; хранение криптографических ключей - в руководстве по Android Keystore.

Связанные темы