DataStore   Часть Android Jetpack.

Попробуйте Kotlin Multiplatform
Kotlin Multiplatform позволяет использовать уровень данных на разных платформах. Как настроить и использовать DataStore в KMP

Jetpack DataStore – это решение для хранения данных, которое позволяет сохранять пары "ключ-значение" или типизированные объекты с помощью буферов протоколов. DataStore использует корутины Kotlin и Flow для асинхронного, согласованного и транзакционного хранения данных.

Если вы используете SharedPreferences для хранения данных, рекомендуем перейти на DataStore.

DataStore API

Интерфейс DataStore предоставляет доступ к следующим API:

  1. Поток, который можно использовать для чтения данных из DataStore.

    val data: Flow<T>
    
  2. Функция для обновления данных в DataStore

    suspend updateData(transform: suspend (t) -> T)
    

Конфигурации DataStore

Если вы хотите хранить данные и получать к ним доступ с помощью ключей, используйте реализацию Preferences DataStore, которая не требует заранее определенной схемы и не обеспечивает безопасность типов. У него есть API, похожий на SharedPreferences, но нет недостатков, связанных с общими параметрами.

DataStore позволяет сохранять пользовательские классы. Для этого необходимо определить схему данных и предоставить Serializer для преобразования их в постоянный формат. Вы можете использовать Protocol Buffers, JSON или любую другую стратегию сериализации.

Настроить

Чтобы использовать Jetpack DataStore в приложении, добавьте в файл Gradle следующие строки в зависимости от того, какую реализацию вы хотите использовать:

Хранилище данных настроек

Добавьте следующие строки в раздел зависимостей вашего Gradle-файла:

Классный

    dependencies {
        // Preferences DataStore (SharedPreferences like APIs)
        implementation "androidx.datastore:datastore-preferences:1.2.1"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.2.1"
    }
    

Котлин

    dependencies {
        // Preferences DataStore (SharedPreferences like APIs)
        implementation("androidx.datastore:datastore-preferences:1.2.1")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.2.1")
    }
    

Для добавления необязательной поддержки RxJava добавьте следующие зависимости:

Классный

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1"
    }
    

Котлин

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.2.1")
    }
    

Хранилище данных

Добавьте следующие строки в раздел зависимостей вашего Gradle-файла:

Классный

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.2.1"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.2.1"
    }
    

Котлин

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.2.1")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.2.1")
    }
    

Для поддержки RxJava добавьте следующие необязательные зависимости:

Классный

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.2.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.2.1"
    }
    

Котлин

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.2.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.2.1")
    }
    

Для сериализации содержимого добавьте зависимости либо для Protocol Buffers, либо для сериализации в формате JSON.

сериализация JSON

Для использования сериализации JSON добавьте в свой файл Gradle следующее:

Классный

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Котлин

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

сериализация Protobuf

Для использования сериализации Protobuf добавьте в свой файл Gradle следующее:

Классный

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Котлин

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Как правильно использовать DataStore

Чтобы правильно использовать DataStore, всегда помните о следующих правилах:

  1. Никогда не создавайте более одного экземпляра DataStore для определенного файла в одном процессе. Это может привести к сбоям в работе DataStore. Если для определенного файла в одном процессе активны несколько хранилищ данных, при чтении или обновлении данных DataStore выдаст ошибку IllegalStateException.

  2. Общий тип DataStore<T> должен быть неизменяемым. Изменение типа, используемого в DataStore, нарушает согласованность, которую обеспечивает DataStore, и может привести к серьезным, трудно обнаруживаемым ошибкам. Мы рекомендуем использовать буферы протоколов, которые обеспечивают неизменяемость, понятный API и эффективную сериализацию.

  3. Не используйте атрибуты "SingleProcessDataStore" и "MultiProcessDataStore" для одного и того же файла. Если вы планируете получить доступ к DataStore из нескольких процессов, вам необходимо использовать MultiProcessDataStore.

Определение данных

Preferences DataStore

Определите ключ, который будет использоваться для сохранения данных на диск.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

Хранилище данных JSON

Для хранилища данных JSON добавьте аннотацию @Serialization к данным, которые нужно сохранить.

@Serializable data class Settings(val exampleCounter: Int)

Определите класс, реализующий Serializer<T>, где T – тип класса, к которому вы добавили аннотацию ранее. Убедитесь, что вы указали значение по умолчанию для сериализатора, который будет использоваться, если файл ещё не создан.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString(Settings.serializer(), input.readBytes().decodeToString())
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Settings", serialization)
        }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        output.write(Json.encodeToString(Settings.serializer(), t).encodeToByteArray())
    }
}

Proto DataStore

В реализации Proto DataStore используются DataStore и буферы протоколов для сохранения типизированных объектов на диск.

Для Proto DataStore требуется заранее определенная схема в файле proto в каталоге app/src/main/proto/. Эта схема определяет тип объектов, которые вы сохраняете в Proto DataStore. Подробнее о том, как определить схему proto, рассказывается в руководстве по языку protobuf.

Добавьте файл settings.proto в папку src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastoresampleapp";
option java_multiple_files = true;

message Settings {
  int32 counter = 1;
  bool foo = 2;
}

Определите класс, реализующий Serializer<T>, где T – тип, определенный в файле proto. Этот класс сериализатора определяет, как DataStore считывает и записывает данные. Убедитесь, что вы добавили значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан.

object SettingsSerializer : Serializer<Settings> {
    override val defaultValue: Settings = Settings.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Settings {
        try {
            return Settings.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: Settings, output: OutputStream) {
        return t.writeTo(output)
    }
}

Как создать DataStore

Вам нужно указать название файла, в котором будут храниться данные.

Preferences DataStore

В реализации Preferences DataStore используются классы DataStore и Preferences для сохранения пар "ключ-значение" на диск. Используйте делегат свойства, созданный preferencesDataStore, чтобы создать экземпляр DataStore<Preferences>. Вызовите его один раз на верхнем уровне файла Kotlin. Используйте это свойство для доступа к DataStore в остальной части приложения. Это упрощает использование DataStore в качестве одноэлементного объекта. Обязательный параметр name – это название Preferences DataStore.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Хранилище данных JSON

Используйте делегат ресурса, созданный dataStore, чтобы создать экземпляр DataStore<T>, где T – это класс сериализуемых данных. Вызовите его один раз на верхнем уровне файла Kotlin и используйте делегат этого свойства во всем приложении. Параметр fileName указывает DataStore, какой файл использовать для хранения данных, а параметр serializer – название класса сериализатора, определенного ранее.

val Context.dataStore: DataStore<Settings> by
    dataStore(
        fileName = "settings.json",
        serializer = SettingsSerializer,
        scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    )

Proto DataStore

Используйте делегат свойства, созданный dataStore, чтобы создать экземпляр DataStore<T>, где T – тип, определенный в файле proto. Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через делегат этого свойства во всем приложении. Параметр fileName указывает DataStore, какой файл использовать для хранения данных, а параметр serializer – название класса сериализатора, определенного ранее.

val Context.dataStore: DataStore<Settings> by
    dataStore(fileName = "settings.pb", serializer = SettingsSerializer)

Чтение данных из хранилища DataStore

Вам нужно указать название файла, в котором будут храниться данные.

Preferences DataStore

Поскольку в Preferences DataStore не используется предопределенная схема, для каждого значения, которое нужно сохранить в экземпляре DataStore<Preferences>, необходимо определить ключ с помощью соответствующей функции типа ключа. Например, чтобы определить ключ для целочисленного значения, используйте intPreferencesKey. Затем используйте свойство DataStore.data, чтобы показать нужное сохраненное значение с помощью Flow.

fun counterFlow(): Flow<Int> =
    context.dataStore.data.map { preferences -> preferences[EXAMPLE_COUNTER] ?: 0 }

Хранилище данных JSON

Используйте DataStore.data, чтобы получить доступ к Flow нужного свойства из сохраненного объекта.

fun counterFlow(): Flow<Int> =
    context.dataStore.data.map { settings -> settings.exampleCounter }

Proto DataStore

Используйте DataStore.data, чтобы получить доступ к Flow нужного свойства из сохраненного объекта.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings -> settings.counter }

Используйте collectAsStateWithLifecycle, чтобы обработать Flow, созданный ViewModel, в composable-функции. Это безопасно преобразует поток DataStore в состояние Compose, которое запускает рекомпозицию.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

Подробнее collectAsStateWithLifecycleо состоянии и Jetpack Compose…

Запись в DataStore

DataStore предоставляет функцию updateData, которая транзакционно обновляет сохраненный объект. updateData возвращает текущее состояние данных в виде экземпляра типа данных и обновляет данные транзакционно в атомарной операции чтения, записи и изменения. Весь код в блоке updateData рассматривается как одна транзакция.

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

Хранилище данных JSON

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.toBuilder().setCounter(settings.counter + 1).build()
    }
}

Как использовать DataStore в приложении Compose

Чтобы использовать DataStore в приложении Compose, следуйте рекомендациям по архитектуре приложений Android: выполняйте операции DataStore на уровне данных (например, в репозитории) и предоставляйте доступ к данным в интерфейсе с помощью ViewModel.

Не считывайте и не записывайте данные в DataStore напрямую из функций, которые можно компоновать.

  1. Предоставьте доступ к DataStore через ViewModel. Передайте репозиторий (который содержит DataStore) в ViewModel и преобразуйте Flow в StateFlow, чтобы интерфейс мог его отслеживать, как показано в следующем фрагменте кода:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. Наблюдайте и пишите из composable-функции. Используйте collectAsStateWithLifecycle, чтобы безопасно наблюдать за StateFlow в интерфейсе, и вызывайте функции ViewModel для обработки операций записи, как показано в следующем фрагменте кода:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

Как использовать DataStore в многопроцессном коде

Вы можете настроить DataStore так, чтобы разные процессы имели доступ к одним и тем же данным с теми же свойствами согласованности, что и в рамках одного процесса. В частности, DataStore предоставляет следующие свойства:

  • При чтении возвращаются только данные, сохраненные на диске.
  • Согласованность при чтении после записи.
  • Записи сериализуются.
  • Операции чтения никогда не блокируются операциями записи.

Предположим, у вас есть приложение с сервисом и действием, где сервис работает в отдельном процессе и периодически обновляет DataStore.

В этом примере используется хранилище данных JSON, но вы также можете использовать Preferences или ProtoDataStore.

@Serializable data class Time(val lastUpdateMillis: Long)

Сериализатор сообщает DataStore, как читать и записывать данные определенного типа. Убедитесь, что вы указали значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан. Ниже приведен пример реализации с использованием kotlinx.serialization:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString(Time.serializer(), input.readBytes().decodeToString())
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(Json.encodeToString(Time.serializer(), t).encodeToByteArray())
    }
}

Чтобы использовать DataStore в разных процессах, вам нужно создать объект DataStore, используя MultiProcessDataStoreFactory для кода приложения и сервисного кода:

val dataStore =
    MultiProcessDataStoreFactory.create(
        serializer = TimeSerializer,
        produceFile = { context.dataStoreFile("time.pb") },
        corruptionHandler = null,
    )

Добавьте в файл AndroidManifiest.xml следующий код:

<service
    android:name="com.example.datastore.snippets.TimestampUpdateService"
    android:exported="false"
    android:process=":service" />

Сервис периодически вызывает updateLastUpdateTime, который записывает данные в хранилище с помощью updateData.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time -> time.copy(lastUpdateMillis = System.currentTimeMillis()) }
}

Приложение считывает значение, записанное сервисом, используя следующий поток данных:

fun timeFlow(): Flow<Long> = dataStore.data.map { time -> time.lastUpdateMillis }

Теперь мы можем объединить все эти функции в класс MultiProcessDataStore и использовать его в приложении.

Вот код сервиса:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

Код приложения:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by
    multiProcessDataStore
        .timeFlow()
        .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(text = "Last updated: $lastUpdateTime", fontSize = 25.sp)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose { context.stopService(serviceIntent) }
}

Вы можете использовать внедрение зависимостей Hilt, чтобы экземпляр DataStore был уникальным для каждого процесса:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Как устранить повреждение файла

В редких случаях постоянный файл DataStore на диске может быть поврежден. По умолчанию DataStore не восстанавливается автоматически после повреждения, и попытки чтения из него приведут к тому, что система выдаст ошибку CorruptionException.

DataStore предлагает API обработчика повреждений, который поможет вам корректно восстановить данные в таком сценарии и избежать исключения. Если обработчик повреждений настроен, он заменяет поврежденный файл новым, содержащим заданное по умолчанию значение.

Чтобы настроить этот обработчик, укажите corruptionHandler при создании экземпляра DataStore в by dataStore или в методе фабрики DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Резервное копирование и восстановление данных с устройства

Файлы Preferences DataStore (*.preferences_pb) и Proto DataStore хранятся в каталоге files/datastore/ приложения. По умолчанию эти файлы включены в автоматическое копирование в облако на устройствах Android и перенос данных между устройствами.

Как настроить правила резервного копирования для DataStore

Если в вашем хранилище данных вместе с конфиденциальными данными содержатся неконфиденциальные настройки (например, тема или флаги функций), разделите их на разные файлы хранилища данных и настройте res/xml/data_extraction_rules.xml следующим образом:

<data-extraction-rules>
    <cloud-backup>
        <!-- Include general settings -->
        <include domain="file" path="datastore/user_settings.preferences_pb"/>
        <!-- Exclude sensitive local state -->
        <exclude domain="file" path="datastore/secure_state.preferences_pb"/>
    </cloud-backup>
    <device-to-device>
        <!-- Transfer settings during device-to-device transfer -->
        <include domain="file" path="datastore/"/>
    </device-to-device>
</data-extraction-rules>

Отправить отзыв

Поделитесь своим мнением и идеями, используя следующие ресурсы:

Система отслеживания ошибок:
Сообщайте о проблемах, чтобы мы могли исправлять ошибки.

Дополнительные ресурсы

Чтобы узнать больше о Jetpack DataStore, ознакомьтесь со следующими дополнительными ресурсами:

Образцы

Блоги

Практические работы