Jetpack DataStore: улучшенная система хранения данных

Что такое DataStore

На протяжении многих лет разработчики Android хранили небольшие фрагменты конфиденциальных пользовательских данных с помощью общих настроек (shared preferences). Этот подход имеет следующие недостатки:

  • Конфиденциальные данные в общих настройках могут быть легко раскрыты.
  • Кажется, что вызывать операции с общими настройками в потоке UI безопасно, но это не так (из-за синхронного API, отсутствия механизма сигнализации ошибок, нехватки транзакционного API и т.д.).

DataStore  —  это библиотека из семейства Jetpack, которая предоставляет новое решение для хранения данных. Скорее всего, оно заменит общие настройки. В настоящее время библиотека находится в альфа-стадии.

Какие подходы она предлагает

Библиотека разработана с использованием корутин Kotlin и Flow API, что делает её более безопасной и надежной, чем общие настройки. Предлагаются два различных подхода к хранению данных:

  • Preferences DataStore. Они похожи на SharedPreferences, поскольку не могут определить схему или обеспечить доступ к ключам с правильным типом.
  • Proto DataStore. С ее помощью можно создавать схемы, используя буферы протокола, которые позволяют сохранять строго типизированные данные. Они быстрее, меньше, проще и менее неоднозначны, чем XML и другие подобные форматы данных.

Кроме того, при хранении пар ключ-значение будут доступны только ключи, а не само содержимое.

DataStore при работе с базами данных

DataStore предназначена для хранения небольших наборов данных. Если в ваши требования входит частичное обновление, ссылочная целостность или поддержка больших и сложных наборов данных, стоит рассмотреть использование Room.

Интеграция DataStore

Чтобы использовать библиотеку Jetpack DataStore, добавьте строку под узлом dependencies в файле build.gradle уровня app.

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

Использование библиотеки на примере

Чтобы лучше понять, как использовать DataStore, я взял простой пример в реальном времени, где мы храним целое число, указывающее статус входа пользователя. Здесь у нас есть класс enum со всеми возможными этапами:

enum class UserStatus {
    STARTER, ONBOARDING_LEVEL_1, ONBOARDING_LEVEL_2, VERIFIED
}

Создание DataStore

Следующим шагом является создание DataStore. Для этого нужно создать класс Kotlin, в котором мы будем использовать функцию расширения context.createDataStore:

class PreferenceManager(context: Context){
    private val dataStore = context.createDataStore(name = "prefernce_name")
}

Создание ключей

Создадим встроенную функцию preferencesKey, определив тип результата:

companion object {
    val USER_STATUS = preferencesKey<Int>("user_status")
}

Сохранение и извлечение данных

Теперь, когда мы создали DataStore и ключи, пришло время сохранить статус пользователя. Сохраним целое число со значениями 1, 2, 3 и 4, представляющими STARTER, ONBOARDING_LEVEL_1, ONBOARDING_LEVEL_2 и VERIFIED.

Чтобы сохранить пары ключ-значение в файле DataStore, воспользуемся функцией edit, которая обновляет значения и suspend для их сохранения:

suspend fun setUserStatus(userStatus: UserStatus) {
    dataStore.edit { preferences ->
        preferences[USER_STATUS] = when (userStatus) {
            UserStatus.STARTER -> 1
            UserStatus.ONBOARDING_LEVEL_1 -> 2
            UserStatus.ONBOARDING_LEVEL_2 -> 3
            UserStatus.VERIFIED -> 4
        }
    }
}

Чтобы извлечь значения из DataStore, мы используем Flow API. Одно из главных преимуществ этого подхода в том, что каждый раз, когда в DataStore обновляется значение, мы получаем уведомление. Таким образом, проверять обновленные значения не потребуется.

val userStatusFlow: Flow<UserStatus> = dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }
        .map { preference ->
            val userStatus = preference[USER_STATUS] ?: 1

            when (userStatus) {
                1 -> UserStatus.STARTER
                2 -> UserStatus.ONBOARDING_LEVEL_1
                3 -> UserStatus.ONBOARDING_LEVEL_2
                4 -> UserStatus.VERIFIED
                else -> UserStatus.STARTER
            }
        }

Вместо того, чтобы передавать целое число в регистр состояния пользователя на сайте вызова, можно использовать такие функции, как map, для преобразования данных в соответствующий тип. Соберем все воедино:

enum class UserStatus {
    STARTER, ONBOARDING_LEVEL_1, ONBOARDING_LEVEL_2, VERIFIED
}

class PreferenceManager(context: Context) {

    private val dataStore = context.createDataStore(name = "prefernce_name")

    val userStatusFlow: Flow<UserStatus> = dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }
        .map { preference ->
            val userStatus = preference[USER_STATUS] ?: 1

            when (userStatus) {
                1 -> UserStatus.STARTER
                2 -> UserStatus.ONBOARDING_LEVEL_1
                3 -> UserStatus.ONBOARDING_LEVEL_2
                4 -> UserStatus.VERIFIED
                else -> UserStatus.STARTER
            }
        }

    suspend fun setUserStatus(userStatus: UserStatus) {
        dataStore.edit { preferences ->
            preferences[USER_STATUS] = when (userStatus) {
                UserStatus.STARTER -> 1
                UserStatus.ONBOARDING_LEVEL_1 -> 2
                UserStatus.ONBOARDING_LEVEL_2 -> 3
                UserStatus.VERIFIED -> 4
            }
        }
    }

    companion object {
        val USER_STATUS = preferencesKey<Int>("user_status")
    }
}

Создание доступа из компонентов Android

Теперь, когда мы закончили с менеджером настроек, разберемся, как вызвать setUserStatus из активности Android:

fun saveuserStatus(status : Int){
  lifecycleScope.launch {
    when (status) {
        1 -> preferenceManager.setUserStatus(UserStatus.STARTER)
        2 -> preferenceManager.setUserStatus(UserStatus.ONBOARDING_LEVEL_1)
        3 -> preferenceManager.setUserStatus(UserStatus.ONBOARDING_LEVEL_2)
        4 -> preferenceManager.setUserStatus(UserStatus.VERIFIED)
    }
  }
}

Поскольку setUserStatus  —  это функция приостановки, для запуска сопрограммы мы использовали lifecycleScope.

Извлечение данных из центра обработки аналогично: здесь мы применяем collectLatest из Flow API, деформируя его с помощью lifecycleScope:

lifecycleScope.launch(Dispatchers.Main) {
    preferencemanager.userStatusFlow.collectLatest { userStatus-> 
        when (userStatus) {
            UserStatus.STARTER -> {/* TODO */}
            UserStatus.ONBOARDING_LEVEL_1 -> {/* TODO */}
            UserStatus.ONBOARDING_LEVEL_2 -> {/* TODO */}
            UserStatus.VERIFIED -> {/* TODO */}
        }
    }
}

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Siva Ganesh Kantamani: Jetpack DataStore: Improved Data-Storage System

Предыдущая статьяОбработка аутентификации и авторизации пользователей после балансировки нагрузки веб-приложения
Следующая статьяПочему я перешёл на Lite после 3-х лет пользования Visual Studio Code