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.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, 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 в качестве синглтона. Обязательный параметр 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 имя класса сериализатора, определенного ранее.

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

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

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

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
}

Используйте collectAsStateWithLifecycle для обработки Flow созданного ViewModel, в составном объекте. Это безопасно преобразует поток DataStore в состояние Compose State, которое запускает перекомпозицию.

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

Для получения дополнительной информации о collectAsStateWithLifecycle см. раздел «Состояние» и Jetpack Compose .

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

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

Используйте 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. Наблюдайте за состоянием вашего компонуемого объекта и записывайте в него данные. Используйте 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 или Proto.

@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.filesDir.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.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

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

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

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

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

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

Образцы

Блоги

Кодлабс

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