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 выдаст исключениеIllegalStateException
при чтении или обновлении данных.Общий тип DataStore
должен быть неизменным. Изменение типа, используемого в DataStore, лишает законной силы любые гарантии, предоставляемые DataStore, и создает потенциально серьезные, труднообнаружимые ошибки. Настоятельно рекомендуется использовать буферы протоколов, которые обеспечивают гарантии неизменности, простой API и эффективную сериализацию. Никогда не используйте одновременно
SingleProcessDataStore
иMultiProcessDataStore
для одного и того же файла. Если вы собираетесь получить доступDataStore
из более чем одного процесса, всегда используйтеMultiProcessDataStore
.
Настраивать
Чтобы использовать Jetpack DataStore в своем приложении, добавьте в файл Gradle следующее в зависимости от того, какую реализацию вы хотите использовать:
Настройки хранилища данных
классный
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.1" }
Котлин
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.1") }
Прото хранилище данных
классный
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.1" }
Котлин
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.1") }
Храните пары ключ-значение с помощью Preferences DataStore.
Реализация Preferences DataStore использует классы DataStore
и Preferences
для сохранения на диске простых пар ключ-значение.
Создайте хранилище данных настроек
Используйте делегат свойства, созданный preferencesDataStore
, чтобы создать экземпляр DataStore<Preferences>
. Вызовите его один раз на верхнем уровне вашего файла Kotlin и получайте к нему доступ через это свойство на протяжении всей остальной части вашего приложения. Это упрощает сохранение DataStore
как одноэлементного. Альтернативно, используйте RxPreferenceDataStoreBuilder
, если вы используете RxJava. Обязательный параметр name
— это имя хранилища данных предпочтений.
Котлин
// 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));
Запись в хранилище данных настроек
Preferences DataStore предоставляет функцию edit()
, которая транзакционно обновляет данные в DataStore
. Параметр 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 требуется предопределенная схема в файле прототипа в каталоге app/src/main/proto/
. Эта схема определяет тип объектов, которые вы сохраняете в своем хранилище данных Proto. Дополнительные сведения об определении протосхемы см. в руководстве по языку protobuf .
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Создайте прототип хранилища данных
Создание Proto DataStore для хранения типизированных объектов состоит из двух шагов:
- Определите класс, реализующий
Serializer<T>
, гдеT
— тип, определенный в файле прототипа. Этот класс сериализатора сообщает DataStore, как читать и записывать ваш тип данных. Обязательно укажите значение по умолчанию для сериализатора, который будет использоваться, если файл еще не создан. - Используйте делегат свойства, созданный
dataStore
, чтобы создать экземплярDataStore<T>
, гдеT
— это тип, определенный в файле прототипа. Вызовите это один раз на верхнем уровне вашего файла 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.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
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 гарантирует:
- Чтение возвращает только те данные, которые были сохранены на диске.
- Согласованность чтения после записи.
- Записи сериализуются.
- Чтение никогда не блокируется записью.
Рассмотрим пример приложения со службой и действием:
Служба работает в отдельном процессе и периодически обновляет хранилище данных.
<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): Timer =
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(...)
Оставьте отзыв
Поделитесь с нами своими отзывами и идеями через эти ресурсы:
- Трекер проблем
- Сообщайте о проблемах, чтобы мы могли исправить ошибки.
Дополнительные ресурсы
Чтобы узнать больше о Jetpack DataStore, см. следующие дополнительные ресурсы:
Образцы
Блоги
Кодлабы
{% дословно %}Рекомендуется для вас
- Примечание. Текст ссылки отображается, когда JavaScript отключен.
- Загрузка и отображение постраничных данных
- Обзор LiveData
- Макеты и выражения привязки