[Разработка под Android, Хранилища данных] Обзор DataStore Library. Прощаемся с SharedPreference?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, меня зовут Сергей, я работаю в команде Мобильного Банка Тинькофф. Недавно Google представила очередной инструмент для хранения данных. На этот раз это библиотека DataStore. В официальном блоге Google пишут, что она должна заменить SharedPreference. В отличие от SharedPreference, DataStore работает асинхронно. Вся работа с библиотекой выполняется с помощью Kotlin Coroutines и Flow. DataStore позволяет нам хранить данные двумя способами:
- По принципу «ключ — значение», аналогично SharedPreference.
- Хранить типизированные объекты, основанные на protocol buffers.
Все взаимодействие с DataStore происходит через интерфейс DataStore<T>, который содержит в себе всего два элемента:
interface DataStore<T> {
val data: Flow<T>
suspend fun updateData(transform: suspend (t: T) -> T): T
}
Интерфейс очень прост. Все, что мы можем сделать с ним, это получить объект Flow<T> для чтения данных и вызвать метод updateData() для их записи.Типы DataStore
- Preferences DataStore — хранит данные по принципу «ключ — значение» и не предоставляет нам никакой типобезопасности.
- Proto DataStore — хранит данные в объектах. Это дает нам типобезопасноть, но описывать схему нужно с помощью protocol buffers.
Поговорим о каждом из них.Preferences DataStore
Для подключения библиотеки необходимо добавить зависимость в build.gradle нашего проекта:
// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
Как получить экземпляр Preferences DataStoreДля этого нам предоставляется extension-функция, которую можно вызвать из объекта Context:
context.createDataStore(
name = "user_data_store",
corruptionHandler = null
migrations = emptyList(),
scope = CoroutineScope(Dispatchers.IO + Job())
)
Здесь есть четыре параметра. Давайте рассмотрим каждый из них.
- name — обязательный параметр. Это название нашего DataStore. Под капотом будет создан файл, путь которого формируется на основании параметра name.
File(context.filesDir, "datastore/" + name + ".preferences_pb")
- corruptionHandler — этот параметр необязательный. CorruptionHandler вызывается, если DataStore бросает CorruptionException при попытке чтения данных. Если CorruptionHandler успешно подменит данные, то исключение будет поглощено. Если в процессе подмены данных мы получим еще одно исключение, то оно будет добавлено к оригинальному исключению, после чего нам будет выброшено оригинальное исключение.
- migrations — необязательный параметр, который позволяет легко мигрировать из SharedPreference. Сюда принимается список объектов DataMigration<Preferences>. На самом деле Google уже создала реализацию SharedPreferencesMigration. Все, что нам нужно, это описать логику переноса данных для каждого Shared Preference и передать их списком в параметр migrations:
fun getSharedPreferenceMigrationPref(): SharedPreferencesMigration<MutablePreferences> =
SharedPreferencesMigration(
context = context,
sharedPreferencesName = "pref_name",
deleteEmptyPreferences = true,
shouldRunMigration = { true },
migrate = { prefs, userPref ->
userPref[FIELD_NAME] = prefs.getString(KEY_NAME)
userPref[FIELD_LAST_NAME] = prefs.getString(KEY_LAST_NAME)
userPref[FIELD_AGE] = prefs.getInt(KEY_AGE, 0)
userPref[FIELD_ACTIVE] = prefs.getBoolean(KEY_IS_ACTIVE, false)
userPref
}
)
В отличие от обычных Shared Preference, в качестве ключа здесь не строка, но об этом мы поговорим чуть позже.
- scope — тоже необязательный параметр. Здесь можно указать, в каком Coroutine Scope мы хотим выполнять операции с DataStore. По умолчанию там Dispatchers.IO.
Создание ключейЧтобы сделать запись в DataStore, нам необходимо определить ключи, под которыми будут храниться наши данные. Как упоминалось выше, это не строки. Поля имеют тип Preferences.Key<T>. Создать подобное поле можно с помощью extension-функции:
object UserScheme {
val FIELD_NAME = preferencesKey<String>("name")
val FIELD_LAST_NAME = preferencesKey<String>("last_name")
val FIELD_AGE = preferencesKey<Int>("age")
val FIELD_ACTIVE = preferencesKey<Boolean>("active")
}
Каждый ключ указывает на тип хранимых в нем данных и строковый ключ, по которому эти данные будут читаться. Поскольку при создании ключа мы указываем тип хранимых данных — мы получаем проверку на корректность передаваемого типа данных в compile time. Стоит помнить, что создавать ключи можно только для примитивных типов данных: Int, Long, Boolean, Float, String. В противном случае мы получим исключение. Также мы можем хранить Set<String>:
val FIELD_STRINGS_SET = preferencesSetKey<Set<String>>("strings_set")
Скорее всего, количество типов будет расширяться, так как сейчас методы prefrencesKey() и prefrencesSetKey() на вход принимают дженерик и ограничение по типам сделано руками.Запись данныхДля записи данных DataStore предоставляет нам два метода для изменения данных:DataStore.updateData
coroutineScope.launch {
prefDataStore.updateData { prefs ->
prefs.toMutablePreferences().apply {
set(UserScheme.FIELD_NAME, "John")
set(UserScheme.FIELD_LAST_NAME, "Show")
set(UserScheme.FIELD_AGE, 100)
set(UserScheme.FIELD_IS_ACTIVE, false)
}
}
}
DataStore.edit
coroutineScope.launch {
prefDataStore.edit { prefs ->
prefs[UserScheme.FIELD_NAME] = "John"
prefs[UserScheme.FIELD_LAST_NAME] = "Show"
prefs[UserScheme.FIELD_AGE] = 100
prefs[UserScheme.FIELD_IS_ACTIVE] = false
}
}
В обоих случаях мы получаем объект Preferences с разницей лишь в том, что во втором случае приведение к мутабельности спрятано под капотом «функции обертки» edit().Preferences очень похожа на Generic Map, в которую мы в качестве ключа указываем определенные нами ранее preferenceKey. Для работы с Preferences есть всего четыре метода get(), contains(), asMap() и set(). Метод set() доступен только в MutablePreferences. Запись в Preferences происходит асинхронно, и корутина завершается после того, как данные сохраняются на диске.Чтение данныхDataStore предоставляет сохраненные данные в объекте Preferences. Все действия производятся на определенном нами при создании Dispatcher:
coroutineScope.launch {
prefDataStore.data
.collect { pref: Preferences ->
val name: String? = pref[UserScheme.FIELD_NAME]
val lastName: String? = pref[UserScheme.FIELD_LAST_NAME]
val age: Int? = pref[UserScheme.FIELD_AGE]
val isActive: Boolean? = pref[UserScheme.FIELD_IS_ACTIVE]
}
}
DataStore возвращает объект Flow, который будет возвращать нам либо значение, либо исключение, в случае ошибки чтения с диска.Proto DataStore
Для подключения добавляем зависимость:
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
Перед работой с Proto DataStore нужно выполнить несколько действий:
- В build.gradle добавить плагин:
plugins {
id "com.google.protobuf" version "0.8.12"
}
- Подключить зависимость в build.gradle:
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
- Создать модель данных, используя protocol buffers.
Для этого нужно создать файл в app/src/main/proto/ с расширением .proto:
syntax = "proto3";
option java_package = "com.example.jetpackdatasource";
option java_multiple_files = true;
message UserProto {
string name = 1;
string last_name = 2;
int32 age = 3;
bool is_active = 4;
}
Здесь есть подробное руководство по работе с proto buffer файлами.Это будет наша схема хранения данных. Система сгенерирует модель, которую мы можем сохранять в наш DataStore.Когда вы все это сделаете, Android Studio предложит установить плагин Protocol Buffer Editor. Он сделает вашу работу с файлами .proto удобной. Плагин будет подсвечивать синтаксические элементы, проводить семантический анализ и др.Как получить экземпляр Proto DataStoreДля этого у нас тоже есть extension-функция:
context.createDataStore(
fileName ="user.pb",
serializer = UserSerializer,
corruptionHandler = null,
migrations = emptyList(),
scope = CoroutineScope(Dispatchers.IO + Job())
)
Здесь все почти то же самое, как и с Preference DataStore. Но есть два отличия:
- Первое — это путь, по которому будет сохраняться файл префов: File(context.filesDir, "datastore/$fileName").
- Второе — наличие поля serializer. Давайте рассмотрим его подробнее. Чтобы Proto DataStore понимал, как ему сохранять данные в файл, мы должны к каждому модели прописать свой Serializer. Для этого нужно реализовать интерфейс Serializer<T>, в котором мы и опишем логику записи/чтения нашего файла:
object UserSerializer : Serializer<User> {
override fun readFrom(input: InputStream): User {
try {
return User.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: User, output: OutputStream) = t.writeTo(output)
}
В остальном тут все так же, как в Preference DataStore.Запись данныхДля записи данных DataStore предоставляет нам функцию DataStore.updateData(). Она возвращает текущее состояние сохраненных данных. В качестве параметра мы получаем экземпляр модели, которую мы определили в файле .proto:
coroutineScope.launch {
protoDataStore.updateData { user ->
user.toBuilder()
.setName(nameField.text.toString())
.setLastName(lastNameField.text.toString())
.setAge(ageField.text.toString().toIntOrNull() ?: 0)
.setIsActive(isActiveSwitch.isChecked)
.build()
}
}
Модель предоставляет нам билдер для записи данных в DataStore. Для каждого поля, указанного в модели, описанной в .proto-файле, мы имеем свой set-метод. Чтение данныхЕсть два способа для чтения данных из Proto DataStore:Вызвать метод DataStore.updateData(). Так как в нем мы получаем актуальное состояние объекта, ничего не мешает прочитать их отсюда. Нюанс в том, что там нужно вернуть актуальное состояние модели в лямбде:
coroutineScope.launch {
protoDataStore.updateData { user ->
val name: String = user.name
val lastName: String = user.lastName
val age: Int = user.age
val isActive: Boolean = user.isActive
return@updateData user
}
}
Получить объект data : Flow<T>, который вернет нам реактивный поток. Результатом этого Flow будет актуальный экземпляр хранимой в DataStore модели:
coroutineScope.launch(Dispatchers.Main) {
protoDataStore.data
.collect { user ->
val receivedUser: User = user
}
}
SharedPreference vs DataStore
- DataStore предоставляет асинхронный API для записи и чтения данных, в отличие от Shared Preference, который предоставляет асинхронный API только при чтении данных.
- DataStore безопасен для работы на UI-потоке, так как есть возможность указать подходящий для нас Dispatcher.
- DataStore защищает от ошибок в рантайме, в то время как Shared Preference может бросить ошибку парсинга в рантайме.
- Proto DataStore предоставляет лучшую типобезопасность из коробки.
Тут стоит отдельно поговорить о транзакционности.В Shared Preference транзакционность может быть достигнута за счет связки edit() -> apply()/commit(). Мы должны получить объект SharedPreferences.Editor, внести изменения и все это зафиксировать методами commit() или apply():
val editor: SharedPreferences.Editor = pref.edit()
editor.putString(KEY_LAST_NAME, lastName)
editor.putBoolean(KEY_IS_ACTIVE, isActive)
editor.apply()
В androidx этот же код будет выглядеть вот так:
pref.edit(commit = false) {
putString(KEY_LAST_NAME, lastName)
putBoolean(KEY_IS_ACTIVE, isActive)
}
По завершении операций в блоке edit{} внутри функции вызовется commit() или apply(), в зависимости от флага commit.DataStore создает транзакцию всякий раз при вызове методов DataStore.updateData() или DataStore.edit() и делает запись после выполнения всех операций внутри этих функций.DataStore vs RoomЕсли вам нужны частичные обновления, ссылочная целостность или поддержка больших/сложных наборов данных, подумайте об использовании Room вместо DataStore.DataStore идеально подходит для небольших простых наборов данных и не поддерживает частичные обновления или ссылочную целостность.Rx JavaВ данный момент поддержки RX Java в DataStore нет. Поэтому, если мы хотим в проект на RX затащить DataStore, придется писать свои обертки. Как вариант, можно использовать тулы для совместимости вроде этой. ВыводУ SharedPreferences есть несколько недостатков:
- Синхронный API, который может показаться безопасным для вызова на UI-потоке, но фактически он выполняет операции дискового ввода-вывода.
- Отсутствует механизм сигнализации об ошибках, транзакционный API и многое другое.
DataStore — это замена SharedPreferences, которая устраняет большинство этих недостатков. DataStore включает в себя полностью асинхронный API, использующий Kotlin Coroutines и Flow. Дает нам очень простой и удобный инструмент для миграции данных. Гарантирует согласованность данных и обработку поврежденных данных.В данный момент библиотека находится в альфе, но вы всегда можете проверить последнюю версию в документации.
===========
Источник:
habr.com
===========
Похожие новости:
- [Серверное администрирование, Резервное копирование, Хранилища данных, Сетевое оборудование] Почему твой бекап недостаточно быстр?
- [Машинное обучение] Генетическое программирование для тестирования компилятора: опыт аспиранта ML-лаборатории ИТМО
- [Open source, C, GitHub, Софт, Будущее здесь] Запуск ОС Андроид с SD-карты для устройств на процессоре Amlogic S912
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Давайте сделаем переиспользуемый компонент tree view в Angular
- [Анализ и проектирование систем, Системы управления версиями, ERP-системы, Хранилища данных] Архитектура ERP-системы реального времени: замещение планов фактами
- [Монетизация игр, Разработка игр, Разработка под Android, Смартфоны] Джентльменский набор от Huawei для разработчика мобильных игр: Game Service и инструменты для быстрой интеграции HMS
- [Платежные системы, Разработка мобильных приложений] Опыт Тинькофф Оплаты: улучшили мобильный SDK и сделали оплату в интернете еще удобнее
- [Big Data, Хранение данных, Хранилища данных, Накопители] Технологии магнитной записи HDD: просто о сложном
- [Карьера в IT-индустрии, Разработка под Android, Учебный процесс в IT] Стоит ли увольнять разраба за большую и дорогую ошибку? Думаю, нет, но менеджмент хотел крови
- [Программирование, Разработка мобильных приложений, Разработка под Android] Большие картинки? Deal with it
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_hranilischa_dannyh (Хранилища данных), #_android, #_sharedpreferences, #_kotlin, #_datasotre, #_blog_kompanii_tinkoff (
Блог компании Tinkoff
), #_razrabotka_pod_android (
Разработка под Android
), #_hranilischa_dannyh (
Хранилища данных
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:50
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, меня зовут Сергей, я работаю в команде Мобильного Банка Тинькофф. Недавно Google представила очередной инструмент для хранения данных. На этот раз это библиотека DataStore. В официальном блоге Google пишут, что она должна заменить SharedPreference. В отличие от SharedPreference, DataStore работает асинхронно. Вся работа с библиотекой выполняется с помощью Kotlin Coroutines и Flow. DataStore позволяет нам хранить данные двумя способами:
interface DataStore<T> {
val data: Flow<T> suspend fun updateData(transform: suspend (t: T) -> T): T }
Для подключения библиотеки необходимо добавить зависимость в build.gradle нашего проекта: // Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01" context.createDataStore(
name = "user_data_store", corruptionHandler = null migrations = emptyList(), scope = CoroutineScope(Dispatchers.IO + Job()) )
File(context.filesDir, "datastore/" + name + ".preferences_pb")
fun getSharedPreferenceMigrationPref(): SharedPreferencesMigration<MutablePreferences> =
SharedPreferencesMigration( context = context, sharedPreferencesName = "pref_name", deleteEmptyPreferences = true, shouldRunMigration = { true }, migrate = { prefs, userPref -> userPref[FIELD_NAME] = prefs.getString(KEY_NAME) userPref[FIELD_LAST_NAME] = prefs.getString(KEY_LAST_NAME) userPref[FIELD_AGE] = prefs.getInt(KEY_AGE, 0) userPref[FIELD_ACTIVE] = prefs.getBoolean(KEY_IS_ACTIVE, false) userPref } )
object UserScheme {
val FIELD_NAME = preferencesKey<String>("name") val FIELD_LAST_NAME = preferencesKey<String>("last_name") val FIELD_AGE = preferencesKey<Int>("age") val FIELD_ACTIVE = preferencesKey<Boolean>("active") } val FIELD_STRINGS_SET = preferencesSetKey<Set<String>>("strings_set")
coroutineScope.launch {
prefDataStore.updateData { prefs -> prefs.toMutablePreferences().apply { set(UserScheme.FIELD_NAME, "John") set(UserScheme.FIELD_LAST_NAME, "Show") set(UserScheme.FIELD_AGE, 100) set(UserScheme.FIELD_IS_ACTIVE, false) } } } coroutineScope.launch {
prefDataStore.edit { prefs -> prefs[UserScheme.FIELD_NAME] = "John" prefs[UserScheme.FIELD_LAST_NAME] = "Show" prefs[UserScheme.FIELD_AGE] = 100 prefs[UserScheme.FIELD_IS_ACTIVE] = false } } coroutineScope.launch {
prefDataStore.data .collect { pref: Preferences -> val name: String? = pref[UserScheme.FIELD_NAME] val lastName: String? = pref[UserScheme.FIELD_LAST_NAME] val age: Int? = pref[UserScheme.FIELD_AGE] val isActive: Boolean? = pref[UserScheme.FIELD_IS_ACTIVE] } } Для подключения добавляем зависимость: // Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
plugins {
id "com.google.protobuf" version "0.8.12" }
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
syntax = "proto3";
option java_package = "com.example.jetpackdatasource"; option java_multiple_files = true; message UserProto { string name = 1; string last_name = 2; int32 age = 3; bool is_active = 4; } context.createDataStore(
fileName ="user.pb", serializer = UserSerializer, corruptionHandler = null, migrations = emptyList(), scope = CoroutineScope(Dispatchers.IO + Job()) )
object UserSerializer : Serializer<User> {
override fun readFrom(input: InputStream): User { try { return User.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override fun writeTo(t: User, output: OutputStream) = t.writeTo(output) } coroutineScope.launch {
protoDataStore.updateData { user -> user.toBuilder() .setName(nameField.text.toString()) .setLastName(lastNameField.text.toString()) .setAge(ageField.text.toString().toIntOrNull() ?: 0) .setIsActive(isActiveSwitch.isChecked) .build() } } coroutineScope.launch {
protoDataStore.updateData { user -> val name: String = user.name val lastName: String = user.lastName val age: Int = user.age val isActive: Boolean = user.isActive return@updateData user } } coroutineScope.launch(Dispatchers.Main) {
protoDataStore.data .collect { user -> val receivedUser: User = user } }
val editor: SharedPreferences.Editor = pref.edit()
editor.putString(KEY_LAST_NAME, lastName) editor.putBoolean(KEY_IS_ACTIVE, isActive) editor.apply() pref.edit(commit = false) {
putString(KEY_LAST_NAME, lastName) putBoolean(KEY_IS_ACTIVE, isActive) }
=========== Источник: habr.com =========== Похожие новости:
Блог компании Tinkoff ), #_razrabotka_pod_android ( Разработка под Android ), #_hranilischa_dannyh ( Хранилища данных ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:50
Часовой пояс: UTC + 5