DataStore — часть Android Jetpack .

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

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

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

API хранилища данных

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

  1. Схема, позволяющая считывать данные из хранилища данных.

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

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

Настройки хранилища данных

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

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

Настраивать

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

Preferences DataStore

将以下行添加到 Gradle 文件的依赖项部分:

Groovy

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

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

Kotlin

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

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

如需添加可选的 RxJava 支持,请添加以下依赖项:

Groovy

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

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

Kotlin

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

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

DataStore

将以下行添加到 Gradle 文件的依赖项部分:

Groovy

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

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

Kotlin

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

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

添加以下可选依赖项以支持 RxJava:

Groovy

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

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

Kotlin

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

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

如需序列化内容,请添加 Protocol Buffers 或 JSON 序列化的依赖项。

JSON 序列化

如需使用 JSON 序列化,请将以下内容添加到 Gradle 文件中:

Groovy

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

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

Kotlin

    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 文件中:

Groovy

    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")
                }
            }
        }
    }
    

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, DataStore выдаст исключение IllegalStateException при чтении или обновлении данных.

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

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

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

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

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

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Для хранилища данных 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>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Прото-хранилище данных

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

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

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

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Определите класс, реализующий интерфейс 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)
    }
}

Создайте хранилище данных.

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

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

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

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

JSON DataStore

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

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

Прото-хранилище данных

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

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

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

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

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

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

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

JSON DataStore

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

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

Прото-хранилище данных

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

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

Запись в хранилище данных

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

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

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

JSON DataStore

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

Прото-хранилище данных

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

Составить образец

Эти функции можно объединить в класс и использовать в приложении Compose.

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

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

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

JSON DataStore

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

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Прото-хранилище данных

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

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

Используйте DataStore в синхронном коде.

Одним из главных преимуществ DataStore является асинхронный API, однако изменить окружающий код для работы в асинхронном режиме не всегда возможно. Это может быть актуально, если вы работаете с существующим кодом, использующим синхронный дисковый ввод-вывод, или если у вас есть зависимость, которая не предоставляет асинхронный API.

Корутины Kotlin предоставляют конструктор корутин runBlocking() , помогающий преодолеть разрыв между синхронным и асинхронным кодом. Вы можете использовать runBlocking() для синхронного чтения данных из DataStore. RxJava предлагает блокирующие методы для Flowable . Следующий код блокирует вызывающий поток до тех пор, пока DataStore не вернет данные:

Котлин

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

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

Котлин

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

Таким образом, DataStore асинхронно считывает данные и кэширует их в памяти. Последующее синхронное чтение с использованием runBlocking() может быть быстрее или вообще избежать операций ввода-вывода на диске, если первоначальное чтение завершилось.

Используйте DataStore в многопроцессном коде.

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

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

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

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

@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>(
                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(t)
                .encodeToByteArray()
        )
    }
}

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

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

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

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

Сервис периодически вызывает 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.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Оставьте отзыв

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

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

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

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

Образцы

Блоги

Кодлабс

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}