DataStore Часть Android Jetpack.
Jetpack DataStore – это решение для хранения данных, которое позволяет сохранять пары "ключ-значение" или типизированные объекты с помощью буферов протоколов. DataStore использует корутины Kotlin и Flow для асинхронного, согласованного и транзакционного хранения данных.
Если вы используете SharedPreferences для хранения данных, рекомендуем перейти на DataStore.
DataStore API
Интерфейс DataStore предоставляет доступ к следующим API:
Поток, который можно использовать для чтения данных из DataStore.
val data: Flow<T>Функция для обновления данных в 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, всегда помните о следующих правилах:
Никогда не создавайте более одного экземпляра
DataStoreдля определенного файла в одном процессе. Это может привести к сбоям в работе DataStore. Если для определенного файла в одном процессе активны несколько хранилищ данных, при чтении или обновлении данных DataStore выдаст ошибкуIllegalStateException.Общий тип
DataStore<T>должен быть неизменяемым. Изменение типа, используемого в DataStore, нарушает согласованность, которую обеспечивает DataStore, и может привести к серьезным, трудно обнаруживаемым ошибкам. Мы рекомендуем использовать буферы протоколов, которые обеспечивают неизменяемость, понятный API и эффективную сериализацию.Не используйте атрибуты "
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 напрямую из функций, которые можно компоновать.
Предоставьте доступ к 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) } } }Наблюдайте и пишите из 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, ознакомьтесь со следующими дополнительными ресурсами:
Образцы
Блоги
Практические работы
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Как загрузить и отобразить данные с разбивкой на страницы
- Общие сведения о LiveData
- Макеты и выражения привязки