DataStore Часть Android Jetpack .
Jetpack DataStore — это решение для хранения данных, позволяющее хранить пары «ключ-значение» или типизированные объекты с помощью буферов протоколов . DataStore использует сопрограммы Kotlin и Flow для асинхронного, согласованного и транзакционного хранения данных.
Если вы в настоящее время используете SharedPreferences
для хранения данных, рассмотрите возможность перехода на DataStore.
Настройки DataStore и Proto DataStore
DataStore предоставляет две различные реализации: Preferences DataStore и Proto DataStore.
- Preferences DataStore хранит данные и обеспечивает доступ к ним с помощью ключей. Эта реализация не требует предопределённой схемы и не обеспечивает типобезопасность.
- Proto DataStore хранит данные как экземпляры пользовательского типа данных. Эта реализация требует определения схемы с использованием буферов протокола , но обеспечивает типобезопасность.
Правильное использование DataStore
Для правильного использования DataStore всегда помните о следующих правилах:
Никогда не создавайте более одного экземпляра
DataStore
для одного файла в одном процессе. Это может привести к нарушению работы DataStore. Если для одного файла в одном процессе активны несколько экземпляров DataStore, DataStore выдаст исключениеIllegalStateException
при чтении или обновлении данных.Обобщённый тип
DataStore<T>
должен быть неизменяемым. Изменение типа, используемого в DataStore, аннулирует все гарантии, предоставляемые DataStore, и создаёт потенциально серьёзные, труднообнаружимые ошибки. Настоятельно рекомендуется использовать буферы протоколов, которые обеспечивают гарантии неизменяемости, простой API и эффективную сериализацию.Никогда не смешивайте использование
SingleProcessDataStore
иMultiProcessDataStore
для одного и того же файла. Если вы планируете обращаться кDataStore
из нескольких процессов, всегда используйтеMultiProcessDataStore
.
Настраивать
Чтобы использовать Jetpack DataStore в своем приложении, добавьте в файл Gradle следующее в зависимости от того, какую реализацию вы хотите использовать:
Настройки хранилища данных
классный
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Котлин
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Прото хранилище данных
классный
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.7" }
Котлин
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.7") }
Храните пары «ключ-значение» с помощью Preferences DataStore
Реализация Preferences DataStore использует классы DataStore
и Preferences
для сохранения простых пар «ключ-значение» на диске.
Создать хранилище данных настроек
Используйте делегат свойства, созданный preferencesDataStore
, для создания экземпляра DataStore<Preferences>
. Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через это свойство во всем остальном приложении. Это упрощает сохранение DataStore
как синглтона. В качестве альтернативы, если вы используете RxJava, используйте RxPreferenceDataStoreBuilder
. Обязательный параметр name
— это имя хранилища данных Preferences.
Котлин
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Ява
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();
Чтение из хранилища данных настроек
Поскольку Preferences DataStore не использует предопределённую схему, необходимо использовать соответствующую функцию типа ключа для определения ключа для каждого значения, которое необходимо сохранить в экземпляре DataStore<Preferences>
. Например, чтобы определить ключ для значения типа int, используйте intPreferencesKey()
. Затем используйте свойство DataStore.data
для предоставления соответствующего сохранённого значения с помощью Flow
.
Котлин
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
Ява
Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter"); Flowable<Integer> exampleCounterFlow = dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));
Запись в хранилище данных настроек
Настройки DataStore предоставляют функцию edit()
, которая транзакционно обновляет данные в DataStore
. Параметр transform
этой функции принимает блок кода, в котором можно обновлять значения по мере необходимости. Весь код в блоке transform рассматривается как одна транзакция.
Котлин
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
Ява
Single<Preferences> updateResult = dataStore.updateDataAsync(prefsIn -> { MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); Integer currentInt = prefsIn.get(INTEGER_KEY); mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1); return Single.just(mutablePreferences); }); // The update is completed once updateResult is completed.
Храните типизированные объекты с помощью Proto DataStore
Реализация Proto DataStore использует DataStore и буферы протокола для сохранения типизированных объектов на диске.
Определить схему
Для Proto DataStore требуется предопределенная схема в proto-файле в каталоге app/src/main/proto/
. Эта схема определяет тип объектов, сохраняемых в Proto DataStore. Подробнее об определении proto-схемы см. в руководстве по языку protobuf .
syntax = "proto3";
option java_package = "com.example.application.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Создать Proto DataStore
Создание Proto DataStore для хранения типизированных объектов состоит из двух этапов:
- Определите класс, реализующий
Serializer<T>
, гдеT
— тип, определённый в proto-файле. Этот класс сериализатора сообщает DataStore, как читать и записывать ваш тип данных. Убедитесь, что вы указали значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан. - Используйте делегат свойства, созданный
dataStore
, для создания экземпляраDataStore<T>
, гдеT
— тип, определённый в proto-файле. Вызовите его один раз на верхнем уровне файла Kotlin и обращайтесь к нему через этот делегат свойства во всём остальном приложении. Параметрfilename
указывает DataStore, какой файл использовать для хранения данных, а параметрserializer
сообщает DataStore имя класса сериализатора, определённого на шаге 1.
Котлин
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) = t.writeTo(output) } val Context.settingsDataStore: DataStore<Settings> by dataStore( fileName = "settings.pb", serializer = SettingsSerializer )
Ява
private static class SettingsSerializer implements Serializer<Settings> { @Override public Settings getDefaultValue() { Settings.getDefaultInstance(); } @Override public Settings readFrom(@NotNull InputStream input) { try { return Settings.parseFrom(input); } catch (exception: InvalidProtocolBufferException) { throw CorruptionException(“Cannot read proto.”, exception); } } @Override public void writeTo(Settings t, @NotNull OutputStream output) { t.writeTo(output); } } RxDataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();
Чтение из Proto DataStore
Используйте DataStore.data
для отображения Flow
соответствующего свойства из вашего сохраненного объекта.
Котлин
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
Ява
Flowable<Integer> exampleCounterFlow = dataStore.data().map(settings -> settings.getExampleCounter());
Запись в Proto DataStore
Proto DataStore предоставляет функцию updateData()
, которая транзакционно обновляет сохраненный объект. updateData()
возвращает текущее состояние данных как экземпляр вашего типа данных и обновляет данные транзакционно в атомарной операции чтения-записи-изменения.
Котлин
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
Ява
Single<Settings> updateResult = dataStore.updateDataAsync(currentSettings -> Single.just( currentSettings.toBuilder() .setExampleCounter(currentSettings.getExampleCounter() + 1) .build()));
Использование 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.
<service android:name=".MyService" android:process=":my_process_id" />
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { while(isActive) { dataStore.updateData { Settings(lastUpdate = System.currentTimeMillis()) } delay(1000) } } }
В то время как приложение будет собирать эти изменения и обновлять свой пользовательский интерфейс
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
Чтобы иметь возможность использовать DataStore в различных процессах, необходимо создать объект DataStore с помощью MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
сообщает DataStore, как читать и записывать ваш тип данных. Убедитесь, что вы указали значение по умолчанию для сериализатора, которое будет использоваться, если файл ещё не создан. Ниже приведён пример реализации с использованием kotlinx.serialization :
@Serializable
data class Settings(
val lastUpdate: Long
)
@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {
override val defaultValue = Settings(lastUpdate = 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()
)
}
}
Вы можете использовать внедрение зависимостей 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
- Макеты и выражения привязки