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 се дефинира веднъж и поддържа добавяне на нови полета без загуба на стари данни. Ако в нова версия на приложението се добави поле със стойност по подразбиране, старият файл ще бъде правилно десериализиран — обратна съвместимост е вградена в протокола.
Изборът между 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 или ръчно да се управляват coroutines. За 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също