Проект: /architecture/_project.yaml Книга: /architecture/_book.yaml Ключевые слова: хранилище данных, архитектура, api:JetpackDataStore Описание: Изучите это руководство по архитектуре приложений в библиотеках уровня данных, чтобы узнать о настройках DataStore и Proto DataStore, настройке и многом другом. hide_page_heading: true
DataStore Часть Android Jetpack .
Jetpack DataStore — это решение для хранения данных, позволяющее хранить пары «ключ-значение» или типизированные объекты с помощью буферов протоколов . DataStore использует сопрограммы Kotlin и Flow для асинхронного, согласованного и транзакционного хранения данных.
Если вы используете SharedPreferences для хранения данных, рассмотрите возможность перехода на DataStore.
API хранилища данных
Интерфейс DataStore предоставляет следующий API:
Поток, который можно использовать для чтения данных из DataStore
val data: Flow<T>Функция обновления данных в DataStore
suspend updateData(transform: suspend (t) -> T)
Конфигурации хранилища данных
Если вы хотите хранить данные и получать к ним доступ с помощью ключей, используйте реализацию 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.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Котлин
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Чтобы добавить дополнительную поддержку RxJava, добавьте следующие зависимости:
Круто
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" }
Котлин
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") }
Хранилище данных
Добавьте следующие строки в раздел зависимостей вашего файла Gradle:
Круто
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.1.7" }
Котлин
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.1.7") }
Добавьте следующие необязательные зависимости для поддержки RxJava:
Круто
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" }
Котлин
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") }
Для сериализации контента добавьте зависимости для 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, DataStore выдаст исключениеIllegalStateExceptionпри чтении или обновлении данных.Обобщённый тип
DataStore<T>должен быть неизменяемым. Изменение типа, используемого в DataStore, нарушает согласованность, обеспечиваемую DataStore, и создаёт потенциально серьёзные, труднообнаружимые ошибки. Мы рекомендуем использовать буферы протоколов, которые обеспечивают неизменяемость, понятный API и эффективную сериализацию.Не смешивайте использование
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
Реализация 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.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 использует классы DataStore и Preferences для сохранения пар «ключ-значение» на диске. Используйте делегат свойства, созданный preferencesDataStore, для создания экземпляра DataStore<Preferences> . Вызовите его один раз на верхнем уровне файла Kotlin. Доступ к DataStore осуществляется через это свойство на протяжении всего оставшегося приложения. Это упрощает сохранение DataStore как отдельного объекта. В качестве альтернативы, используйте RxPreferenceDataStoreBuilder , если вы используете RxJava. Обязательный параметр name — это имя Preferences DataStore.
// 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,
)
Proto DataStore
Используйте делегат свойства, созданный dataStore для создания экземпляра DataStore<T> , где T — тип, определённый в proto-файле. Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через этот делегат свойства во всём остальном приложении. Параметр fileName указывает DataStore, какой файл использовать для хранения данных, а параметр serializer сообщает DataStore имя класса сериализатора, определённого на шаге 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
)
Чтение из DataStore
Вам необходимо указать имя файла, который будет использоваться для сохранения данных.
Настройки хранилища данных
Поскольку Preferences DataStore не использует предопределённую схему, необходимо использовать соответствующую функцию типа ключа для определения ключа для каждого значения, которое необходимо сохранить в экземпляре DataStore<Preferences> . Например, чтобы определить ключ для значения типа int, используйте 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
}
Proto DataStore
Используйте DataStore.data для отображения Flow соответствующего свойства из вашего сохраненного объекта.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Запись в DataStore
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)
}
}
Proto DataStore
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")
}
Proto DataStore
Теперь мы можем поместить эти функции в класс 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() }
Ява
Settings settings = dataStore.data().blockingFirst();
Выполнение синхронных операций ввода-вывода в потоке пользовательского интерфейса может привести к ошибкам ANR или зависанию пользовательского интерфейса. Эти проблемы можно устранить, выполнив асинхронную предварительную загрузку данных из DataStore:
Котлин
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
context.dataStore.data.first()
// You should also handle IOExceptions here.
}
}
Ява
dataStore.data().first().subscribe();
Таким образом, DataStore асинхронно считывает данные и кэширует их в памяти. Последующие синхронные чтения с использованием runBlocking() могут выполняться быстрее или вообще исключать операцию дискового ввода-вывода, если первоначальное чтение уже завершено.
Используйте DataStore в многопроцессном коде
Вы можете настроить DataStore для доступа к одним и тем же данным из разных процессов с теми же свойствами согласованности данных, что и внутри одного процесса. В частности, DataStore обеспечивает:
- Чтение возвращает только те данные, которые были сохранены на диске.
- Согласованность чтения после записи.
- Записи сериализуются.
- Чтение никогда не блокируется записью.
Рассмотрим пример приложения со службой и действием, где служба работает в отдельном процессе и периодически обновляет DataStore.
В этом примере используется хранилище данных JSON, но вы также можете использовать хранилище настроек или 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.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, ознакомьтесь со следующими дополнительными ресурсами:
Образцы
Блоги
Codelabs
{% дословно %}Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Загрузка и отображение постраничных данных
- Обзор LiveData
- Макеты и выражения привязки
