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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також