DataStore для Kotlin: полное руководство с примерами
1. Что это и зачем нужна
DataStore — это библиотека от Google для хранения небольших объёмов данных в приложениях на Kotlin (и Android). Она пришла на смену устаревшей SharedPreferences и предлагает более безопасный, асинхронный и типобезопасный способ работы с данными. DataStore использует Kotlin Coroutines и Flow, что делает её идеальным выбором для современных приложений, написанных с использованием реактивного программирования.
Зачем нужна DataStore? Основная проблема SharedPreferences — это синхронное чтение с диска, которое может блокировать UI-поток, а также отсутствие поддержки типов данных (всё хранится как Set<String>). DataStore решает эти проблемы: все операции чтения и записи асинхронны, а данные хранятся в типизированных объектах (через Protocol Buffers или Preferences). Кроме того, DataStore автоматически обрабатывает ошибки сериализации и предоставляет чистый API для работы с данными.
Библиотека существует в двух вариантах: Preferences DataStore (для хранения простых пар "ключ-значение", как в SharedPreferences) и Proto DataStore (для хранения типизированных объектов через Protocol Buffers). В этой статье мы сфокусируемся на Preferences DataStore, как на самом простом и часто используемом варианте.
2. Установка
DataStore распространяется через Maven Central. Для добавления в проект Kotlin/Android добавьте зависимость в файл build.gradle.kts (модуль app):
dependencies {
// Preferences DataStore
implementation("androidx.datastore:datastore-preferences:1.1.1")
// Если вы используете Proto DataStore
// implementation("androidx.datastore:datastore-core:1.1.1")
// Для поддержки Coroutines (обычно уже есть в проекте)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
}
Если вы используете Gradle с Groovy (build.gradle), то строка будет выглядеть так:
implementation 'androidx.datastore:datastore-preferences:1.1.1'
После синхронизации Gradle библиотека готова к использованию.
3. Быстрый старт — минимальный рабочий пример
Создадим простой DataStore для хранения имени пользователя и счётчика.
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// Расширение для Context, создающее DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_settings")
// Ключи для данных
val USER_NAME_KEY = stringPreferencesKey("user_name")
val COUNTER_KEY = intPreferencesKey("counter")
class UserPreferences(private val context: Context) {
// Чтение имени пользователя как Flow
val userName: Flow<String?> = context.dataStore.data.map { preferences ->
preferences[USER_NAME_KEY]
}
// Чтение счётчика как Flow
val counter: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[COUNTER_KEY] ?: 0
}
// Сохранение имени пользователя
suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME_KEY] = name
}
}
// Увеличение счётчика
suspend fun incrementCounter() {
context.dataStore.edit { preferences ->
val currentCounter = preferences[COUNTER_KEY] ?: 0
preferences[COUNTER_KEY] = currentCounter + 1
}
}
}
// Использование в Activity или ViewModel
// val userPrefs = UserPreferences(this)
// lifecycleScope.launch {
// userPrefs.userName.collect { name -> println("User: $name") }
// userPrefs.saveUserName("Alice")
// }
Этот пример показывает базовую структуру: создание DataStore, определение ключей, чтение через Flow и запись через корутины.
4. Основные методы/функции/классы
4.1. preferencesDataStore
Сигнатура: fun Context.preferencesDataStore(name: String): ReadOnlyProperty<Context, DataStore<Preferences>>
Что делает: Создаёт экземпляр DataStore для хранения пар "ключ-значение". Вызывается один раз на уровне модуля.
Пример:
val Context.myDataStore: DataStore<Preferences> by preferencesDataStore(name = "my_preferences")
4.2. DataStore.data
Сигнатура: val data: Flow<Preferences>
Что делает: Возвращает Flow всех сохранённых данных. Каждый раз, когда данные изменяются, Flow испускает новое значение.
Пример:
context.myDataStore.data.collect { preferences ->
val value = preferences[intPreferencesKey("my_key")]
println("Current value: $value")
}
4.3. DataStore.edit
Сигнатура: suspend fun edit(transform: suspend (MutablePreferences) -> Unit)
Что делает: Атомарно изменяет данные в DataStore. Внутри лямбды вы получаете MutablePreferences и можете добавлять/удалять/изменять значения.
Пример:
context.myDataStore.edit { mutablePrefs ->
mutablePrefs[intPreferencesKey("counter")] = 42
mutablePrefs.remove(stringPreferencesKey("temp_key"))
}
4.4. intPreferencesKey
Сигнатура: fun intPreferencesKey(name: String): Preferences.Key<Int>
Что делает: Создаёт типизированный ключ для хранения целых чисел. Аналогичные функции есть для String, Boolean, Float, Double, Long, Set<String>.
Пример:
val scoreKey = intPreferencesKey("high_score")
val nameKey = stringPreferencesKey("player_name")
val isPremiumKey = booleanPreferencesKey("is_premium")
4.5. stringPreferencesKey
Сигнатура: fun stringPreferencesKey(name: String): Preferences.Key<String>
Что делает: Создаёт ключ для хранения строковых значений.
Пример:
val tokenKey = stringPreferencesKey("auth_token")
// Использование:
context.dataStore.edit { it[tokenKey] = "new_token" }
4.6. booleanPreferencesKey
Сигнатура: fun booleanPreferencesKey(name: String): Preferences.Key<Boolean>
Что делает: Создаёт ключ для хранения булевых значений.
Пример:
val darkModeKey = booleanPreferencesKey("dark_mode_enabled")
// Чтение:
val isDarkMode: Flow<Boolean> = dataStore.data.map { it[darkModeKey] ?: false }
4.7. floatPreferencesKey / doublePreferencesKey / longPreferencesKey
Сигнатура: fun floatPreferencesKey(name: String): Preferences.Key<Float> (аналогично для Double и Long)
Что делает: Создаёт ключи для хранения чисел с плавающей точкой и длинных целых.
Пример:
val volumeKey = floatPreferencesKey("volume_level")
val lastSyncKey = longPreferencesKey("last_sync_timestamp")
4.8. stringSetPreferencesKey
Сигнатура: fun stringSetPreferencesKey(name: String): Preferences.Key<Set<String>>
Что делает: Создаёт ключ для хранения множества строк (например, список тегов или избранных элементов).
Пример:
val tagsKey = stringSetPreferencesKey("user_tags")
// Сохранение:
dataStore.edit { it[tagsKey] = setOf("kotlin", "android", "datastore") }
// Чтение:
val tags: Flow<Set<String>> = dataStore.data.map { it[tagsKey] ?: emptySet() }
4.9. Preferences.asMap()
Сигнатура: fun Preferences.asMap(): Map<Preferences.Key<*>, Any?>
Что делает: Преобразует все сохранённые данные в обычную карту для отладки или итерации.
Пример:
dataStore.data.collect { preferences ->
val allData = preferences.asMap()
allData.forEach { (key, value) ->
println("${key.name} = $value")
}
}
4.10. DataStore.updateData (для Proto DataStore)
Сигнатура: suspend fun updateData(transform: suspend (T) -> T): T
Что делает: Атомарно обновляет типизированные данные в Proto DataStore. Возвращает новое значение.
Пример (для Proto DataStore):
data class UserSettings(val name: String, val age: Int)
val settingsDataStore: DataStore<UserSettings> = // инициализация
settingsDataStore.updateData { currentSettings ->
currentSettings.copy(age = currentSettings.age + 1)
}
5. Реальный практический пример (100-200 строк кода)
Создадим полноценный менеджер настроек для приложения-читалки. Он будет хранить тему, размер шрифта, последнюю открытую книгу и список закладок.
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// Расширение для DataStore
val Context.readerDataStore: DataStore<Preferences> by preferencesDataStore(name = "reader_settings")
// Ключи
object ReaderKeys {
val THEME = stringPreferencesKey("theme") // "light" или "dark"
val FONT_SIZE = intPreferencesKey("font_size")
val LAST_BOOK_ID = stringPreferencesKey("last_book_id")
val BOOKMARKS = stringPreferencesKey("bookmarks") // JSON-строка
val AUTO_SCROLL = booleanPreferencesKey("auto_scroll")
val SCROLL_SPEED = floatPreferencesKey("scroll_speed")
val LAST_READ_POSITION = longPreferencesKey("last_read_position")
val FONT_FAMILY = stringPreferencesKey("font_family")
}
class ReaderPreferences(private val context: Context) {
// Потоки для чтения
val theme: Flow<String> = context.readerDataStore.data.map { prefs ->
prefs[ReaderKeys.THEME] ?: "light"
}
val fontSize: Flow<Int> = context.readerDataStore.data.map { prefs ->
prefs[ReaderKeys.FONT_SIZE] ?: 16
}
val lastBookId: Flow<String?> = context.readerDataStore.data.map { prefs ->
prefs[ReaderKeys.LAST_BOOK_ID]
}
val bookmarks: Flow<List<String>> = context.readerDataStore.data.map { prefs ->
val json = prefs[ReaderKeys.BOOKMARKS] ?: "[]"
parseBookmarks(json)
}
val autoScroll: Flow<Boolean> = context.readerDataStore.data.map { prefs ->
prefs[ReaderKeys.AUTO_SCROLL] ?: false
}
val scrollSpeed: Flow<Float> = context.readerDataStore.data.map { prefs ->
prefs[ReaderKeys.SCROLL_SPEED] ?: 1.0f
}
// Методы для записи
suspend fun setTheme(theme: String) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.THEME] = theme
}
}
suspend fun setFontSize(size: Int) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.FONT_SIZE] = size.coerceIn(10, 40)
}
}
suspend fun setLastBook(bookId: String) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.LAST_BOOK_ID] = bookId
}
}
suspend fun addBookmark(pageId: String) {
context.readerDataStore.edit { prefs ->
val current = parseBookmarks(prefs[ReaderKeys.BOOKMARKS] ?: "[]")
val updated = current + pageId
prefs[ReaderKeys.BOOKMARKS] = toJson(updated)
}
}
suspend fun removeBookmark(pageId: String) {
context.readerDataStore.edit { prefs ->
val current = parseBookmarks(prefs[ReaderKeys.BOOKMARKS] ?: "[]")
val updated = current - pageId
prefs[ReaderKeys.BOOKMARKS] = toJson(updated)
}
}
suspend fun setAutoScroll(enabled: Boolean) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.AUTO_SCROLL] = enabled
}
}
suspend fun setScrollSpeed(speed: Float) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.SCROLL_SPEED] = speed.coerceIn(0.5f, 5.0f)
}
}
suspend fun setLastPosition(position: Long) {
context.readerDataStore.edit { prefs ->
prefs[ReaderKeys.LAST_READ_POSITION] = position
}
}
suspend fun clearAll() {
context.readerDataStore.edit { prefs ->
prefs.clear()
}
}
// Вспомогательные функции для сериализации
private fun parseBookmarks(json: String): List<String> {
return try {
// Используем простой парсинг, в реальности лучше использовать kotlinx.serialization
json.removeSurrounding("[", "]")
.split(",")
.map { it.trim().removeSurrounding("\"") }
. Также в библиотеке
Event Bus и паттерн Observer в Swift: реализация и примеры
Генератор QR-кодов на Kotlin: создание и настройка QR-кодов
Работа с CSV в Go (Golang): полное руководство по парсингу и записи
Отправка SMS на TypeScript: API библиотеки sms-client-ts