Хабрахабр

[Из песочницы] Android preferences delegate

Те кто хочет посмотреть результат, может перейти к готовому решению, остальным добро пожаловать под кат. В данной статье разобран пример создания делегата для SharedPreferences, который уменьшает boilerplate и делает использование SharedPrefernces более удобным.

Основные способы для этого: хранить на сервере или в файлах на исполняемом устройстве. Одной из насущных задач разработки под android является сохранение между сессиями приложение каких-либо данных. Одним из самых первых способов с котором знакомиться любой начинающий разработчик на android это хранение в файле при помощи уже готового инструмента SharedPreferences.

Допустим что нам нужно записывать имя пользователя и далее его отображать где либо в приложении.

class UserStore(private val preferences: SharedPreferences) fun saveUserName(userName: String) { preferences.edit().putString(USER_NAME, userName).apply() } companion object { private const val USER_NAME = "user_name" }
}

Что такое делегат и как его готовят

Кто хочет узнать большее официальная документация. В двух словах это класс, который инкапсулирует установку и получение свойства.

Передаем SharedPreferences, ключ по которому будет храниться свойство и дефолтное значение через конструктор. Что бы сделать класс делегатом необходимо реализовать интерфейс ReadOnlyProperty для val и ReadWriteProperty для var. В setValue устанавливаем значение в getValue получаем значение.

class StringPreferencesDelegate( private val preferences: SharedPreferences, private val name: String, private val defValue: String
) : ReadWriteProperty<Any?, String?> { override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { preferences.edit().putString(name, value).apply() } override fun getValue(thisRef: Any?, property: KProperty<*>): String? { return preferences.getString(name, defValue) }
}

Применяем делегат

class UserStore(private val preferences: SharedPreferences) { var userName: String by StringPreferencesDelegate(preferences, USER_NAME, "") companion object { private const val USER_NAME = "user_name" }
}

Теперь каждый раз, когда данное свойство будет запрашиваться или устанавливаться будут запускаться методы getValue и setValue созданного делегата. Назначение свойству делегата осуществляется по ключевому слову by.

Так же аналогично теперь можно поступить с другими полями, к примеру, если нужно так же сохранять телефон пользователя.

var userPhone: String by StringPreferencesDelegate(preferences, USER_PHONE, "")

Generic

Чтобы не делать для каждого типа данных отдельный делегат воспользуемся обобщениями generic официальная документация.

Для него определяется конкретный тип данных с которым он работает. Обычно первое не осознанное знакомство с generic происходит при создании экземпляра класса List.

val names :List<String> = listOf("Jon","Bob","Max")

Чтобы задать обобщенный тип данных у класса после его название необходимо указать название этой переменной в угловых скобках.

PreferencesDelegate<TValue>(...)

Теперь необходимо задать, что устанавливаемое значение и дефолтное значение имеют тип TValue.

class PreferencesDelegate<TValue>( val preferences: SharedPreferences, private val name: String, private val defValue: TValue
) : ReadWriteProperty<Any?, TValue> { override fun getValue(thisRef: Any?, property: KProperty<*>): TValue { ... } override fun setValue(thisRef: Any?, property: KProperty<*>, value: TValue) { ... }
}

Соответственно теперь создание экземпляра класса выглядит так:

var userName: String? by StringPreferencesDelegate<String?>(...)

Осталось сделать маппинг получения и установки свойств, определяем тип данных по delaultValue, заодно это дает smart cast этого значения к конкретному типу данных, если что то пошло не так и свойство не типа TValue возвращаем defValue.

override fun getValue(thisRef: Any?, property: KProperty<*>): TValue { with(preferences) { return when (defValue) { is Boolean -> (preferences.getBoolean(name, defValue) as? TValue) ?: defValue ... } } } override fun setValue(thisRef: Any?, property: KProperty<*>, value: TValue) { with(preferences.edit()) { when (value) { is Boolean -> putBoolean(name, value) ... } apply() } }

С остальными типами данных аналогично.

Кастомная ошибка

Если произойдет исключение, тогда будет максимально понятно, что произошло. Остается вопрос, что делать с веткой else, поскольку тип TValue может быть абсолютно любым.
Хорошим тоном будет сделать свою кастомную ошибку.

class NotFoundRealizationException(value: Any?) : Exception("not found realization for ${value?.javaClass}") ... else -> throw NotFoundRealizationException(value) ...

Заключение

Итого получаем готовый к применению делегат:

@Suppress("UNCHECKED_CAST")
class PreferencesDelegate<TValue>( val preferences: SharedPreferences, private val name: String, private val defValue: TValue
) : ReadWriteProperty<Any?, TValue> { override fun getValue(thisRef: Any?, property: KProperty<*>): TValue { with(preferences) { return when (defValue) { is Boolean -> (getBoolean(name, defValue) as? TValue) ?: defValue is Int -> (getInt(name, defValue) as TValue) ?: defValue is Float -> (getFloat(name, defValue) as TValue) ?: defValue is Long -> (getLong(name, defValue) as TValue) ?: defValue is String -> (getString(name, defValue) as TValue) ?: defValue else -> throw NotFoundRealizationException(defValue) } } } override fun setValue(thisRef: Any?, property: KProperty<*>, value: TValue) { with(preferences.edit()) { when (value) { is Boolean -> putBoolean(name, value) is Int -> putInt(name, value) is Float -> putFloat(name, value) is Long -> putLong(name, value) is String -> putString(name, value) else -> throw NotFoundRealizationException(value) } apply() } } class NotFoundRealizationException(defValue: Any?) : Exception("not found realization for $defValue")
}

Пример применения

class UserStore(private val preferences: SharedPreferences) { var userName: String by PreferencesDelegate(preferences, USER_NAME, "") var userPhone: String by PreferencesDelegate(preferences, USER_PHONE, "") var isShowLicence: Boolean by PreferencesDelegate(preferences, USER_LICENCE, false) companion object { private const val USER_NAME = "user_name" private const val USER_PHONE = "user_phone" private const val USER_LICENCE = "user_licence" }
}

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть