Русский
Русский
English
Статистика
Реклама

Обзор DataStore Library. Прощаемся с SharedPreference?

Привет, меня зовут Сергей, я работаю в команде Мобильного Банка Тинькофф. Недавно 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 DataStoreimplementation "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 DataStoreimplementation  "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"

Для этого нужно создать файл в 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
К списку статей
Опубликовано: 28.10.2020 10:05:26
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании tinkoff

Разработка под android

Хранилища данных

Android

Sharedpreferences

Kotlin

Datasotre

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru