DataStore для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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("\"") }
                .                    

Рекомендаций