DataStore — компонент из библиотеки Jetpack, предназначенный для хранения небольших объёмов данных в Android-приложениях. В отличие от SharedPreferences, он работает асинхронно и гарантирует согласованность данных при конкурентном доступе. По данным Google, 2024, DataStore использует Kotlin Coroutines и Flow, что делает его безопасным для главного потока и подходящим для реактивных архитектур.
Главное
DataStore — решение от Google для локального хранения данных в Android, представленное в 2020 году как альтернатива SharedPreferences. Он поддерживает два режима: Preferences DataStore (простые пары ключ-значение) и Proto DataStore (типизированная схема на основе Protocol Buffers).
Основное преимущество — полная асинхронность: все операции чтения возвращают Flow из Kotlin Coroutines, а запись выполняется в coroutine-контексте. Это исключает блокировку главного потока, которая была типичной проблемой SharedPreferences при работе с большими объёмами данных.
DataStore гарантирует атомарность операций: конкурентные записи не приводят к потере данных благодаря транзакционной модели. Если два компонента одновременно изменяют одно и то же значение, DataStore корректно обрабатывает конфликт через механизм compare-and-swap.
По данным Google I/O 2023, DataStore используется в 40% новых проектов на Android, и Google рекомендует мигрировать с SharedPreferences во всех приложениях, где требуется стабильность хранения настроек.
В основе DataStore лежит SingleProcessDataStore — реализация, работающая в рамках одного процесса. Она использует файловое хранилище с блокировками на уровне файла: при записи данных файл блокируется, что предотвращает повреждение при конкурентном доступе.
DataStore автоматически обрабатывает ошибки десериализации: если файл повреждён, он возвращает значение по умолчанию и перезаписывает файл. Это поведение настраивается через corruptionHandler, который можно задать при создании DataStore.
SharedPreferences страдает от трёх фундаментальных проблем: синхронное чтение с диска на главном потоке, отсутствие гарантий атомарности при конкурентных записях и невозможность реактивного отслеживания изменений. DataStore решает все три: Flow для наблюдения, файловая блокировка для атомарности и асинхронное API для безопасности потоков.
DataStore хранит данные в файлах на внутреннем хранилище устройства. Preferences DataStore использует формат файла, аналогичный SharedPreferences, но с дополнительными метаданными для проверки целостности. Proto DataStore использует бинарный формат Protocol Buffers, что уменьшает размер файла и ускоряет сериализацию.
При чтении данных DataStore загружает весь файл в память однократно, после чего подписчики получают актуальное состояние через Flow. Изменения транслируются всем активным подписчикам автоматически — не требуется ручная регистрация listener-ов, как в SharedPreferences.
Preferences DataStore использует встроенный механизм сериализации на основе Map. Каждая запись — пара строки и примитивного типа (Int, Boolean, Float, Long, String, Set). Данные хранятся в XML-файле, аналогичном SharedPreferences, но с атомарной записью через файловую блокировку.
Пример создания Preferences DataStore: расширение preferencesDataStore на Context создаёт синглтон с именем файла. При повторных вызовах возвращается тот же экземпляр — это исключает дублирование файлов и путаницу с разными экземплярами хранилища.
Proto DataStore требует определения схемы данных через .proto-файл и компиляции с помощью protobuf-плагина. Сгенерированный Java-класс используется как единственная точка входа для всех полей — это исключает опечатки в ключах, типичные для SharedPreferences.
Схема Proto DataStore задаётся один раз и поддерживает добавление новых полей без потери старых данных. Если в новой версии приложения добавить поле с default-значением, старый файл будет корректно десериализован — обратная совместимость встроена в протокол.
Выбор между Preferences DataStore и Proto DataStore зависит от сложности данных и требований к типизации. Оба варианта асинхронны и транзакционны, но различаются по уровню type-safety и производительности сериализации.
| Характеристика | Preferences DataStore | Proto DataStore |
|---|---|---|
| Типизация | Слабая (ключ-значение) | Строгая (сгенерированный класс) |
| Сериализация | XML (встроенная) | Protocol Buffers (protobuf) |
| Размер файла | Большой (читаемый XML) | Малый (бинарный) |
| Сложность | Низкая (без .proto) | Средняя (нужен .proto) |
| Миграция схемы | Нет схемы | Автоматическая (proto) |
| Совместимость | SharedPreferences (через миграцию) | Только Proto DataStore |
Preferences DataStore подходит для простых настроек: флаги включения функций, строка токена авторизации, число запусков приложения. Если данных мало (до 10–15 ключей) и они не требуют строгой схемы — Preferences DataStore даёт минимальный порог входа без подключения protobuf-плагина.
Proto DataStore оправдан, когда структура данных сложная или может меняться между версиями приложения. Например, настройки профиля пользователя или конфигурация A/B-тестов с 20+ полями. Protobuf даёт строгую типизацию и автоматические миграции, что исключает ошибки времени выполнения из-за несовпадения ключей.
Google предоставляет встроенный механизм миграции через класс SharedPreferencesMigration. Миграция выполняется однократно при первом запуске после обновления приложения: DataStore читает данные из SharedPreferences, записывает их в свой формат и помечает миграцию завершённой.
Миграция поддерживает кастомные трансформации: если в SharedPreferences ключи не совпадают с желаемыми ключами DataStore, можно задать функцию преобразования через SharedPreferencesMigration. Это позволяет переименовывать ключи и менять типы данных в процессе миграции.
Первым шагом добавьте DataStore в build.gradle и создайте экземпляр DataStore с миграцией: SharedPreferencesMigration принимает имя SharedPreferences-файла и набор ключей, которые нужно перенести. Вторым шагом удалите весь код, работающий через SharedPreferences, и замените его вызовами DataStore. Третьим — протестируйте миграцию: при первом запуске данные должны появиться в DataStore, а старый SharedPreferences-файл — перестать использоваться.
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs")
)
}
)
DataStore легко интегрируется в существующий проект. Ниже приведены практические примеры для Preferences DataStore и Proto DataStore — оба демонстрируют чтение, запись и реактивное наблюдение за данными.
В этом примере Preferences DataStore хранит три настройки: тёмную тему, имя пользователя и количество запусков. Чтение выполняется через расширение .data, которое возвращает Flow. Запись — через suspend-функцию .edit, гарантирующую атомарность изменений.
val Context.settingsDataStore by preferencesDataStore(name = "settings")
val isDarkMode: Flow<Boolean> = settingsDataStore.data
.map { preferences ->
preferences[booleanPreferencesKey("dark_mode")] ?: false
}
suspend fun toggleDarkMode() {
settingsDataStore.edit { prefs ->
val current = prefs[booleanPreferencesKey("dark_mode")] ?: false
prefs[booleanPreferencesKey("dark_mode")] = !current
}
}
Proto DataStore требует определения .proto-файла. После компиляции создаётся класс UserSettings, который используется для чтения и записи. Миграции версий схемы описываются в том же .proto-файле и применяются автоматически.
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
string display_name = 1;
int32 notification_count = 2;
bool notifications_enabled = 3;
}
// Чтение из DataStore
val userPreferencesFlow: Flow<UserPreferences> =
protoDataStore.data
// Запись новых значений
suspend fun updateDisplayName(name: String) {
protoDataStore.updateData { prefs ->
prefs.toBuilder()
.setDisplayName(name)
.build()
}
}
DataStore интегрируется с архитектурой MVVM через ViewModel. Flow из DataStore собирается через .stateIn и используется в UI. При каждом изменении данных UI обновляется автоматически — не нужны ручные обновления или LiveData.
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val uiState: StateFlow<SettingsUiState> =
dataStore.data
.map { prefs ->
SettingsUiState(
isDarkMode = prefs[booleanPreferencesKey("dark_mode")] ?: false,
counter = prefs[intPreferencesKey("launch_count")] ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SettingsUiState()
)
}
Часто задаваемые вопросы
DataStore работает асинхронно (не блокирует UI-поток), поддерживает конкурентный доступ через транзакции и позволяет реактивно подписываться на изменения через Flow. SharedPreferences — синхронное API с риском ANR при больших объёмах данных и без встроенной поддержки реактивности.
DataStore написан на Kotlin и требует Kotlin Coroutines. Использовать его из Java можно, но это неудобно: придётся создавать обёртки с CompletableFuture или вручную управлять корутинами. Для Java-проектов Google рекомендует оставить SharedPreferences или добавить Kotlin в модуль.
DataStore загружает весь файл в память при чтении, поэтому не подходит для хранения списков или больших объектов. Для таких сценариев используйте Room или SQLite. DataStore оптимизирован для настроек и небольших структурированных данных — до сотен килобайт.
При создании DataStore можно передать corruptionHandler — функцию, которая вызывается при повреждении файла. По умолчанию DataStore выбрасывает исключение CorruptionException. В corruptionHandler можно вернуть пустые данные, после чего DataStore перезапишет файл корректным состоянием.
Да, Proto DataStore требует определения схемы в .proto-файле и подключения плагина protobuf-gradle-plugin. Если проект небольшой и данные простые, проще использовать Preferences DataStore — он не требует дополнительной конфигурации сборки.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также