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 следующее, в зависимости от того, какую реализацию вы хотите использовать:

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

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

Круто

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

Котлин

    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, добавьте следующие зависимости:

Круто

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

Котлин

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

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

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

Круто

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

Котлин

    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:

Круто

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

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

Котлин

    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 следующее:

Круто

    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, 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 %}