Реализация Android Preferences Delegate на Kotlin
В данной статье разобран пример создания делегата для SharedPreferences, который уменьшает boilerplate и делает использование SharedPreferences более удобным. Те, кто хочет посмотреть результат, могут перейти к готовому решению в конце статьи, остальным добро пожаловать под кат.
Одной из насущных задач разработки под Android является сохранение между сессиями приложения каких-либо данных. Основные способы для этого: хранить на сервере или в файлах на исполняемом устройстве. Одним из самых первых способов, с котором знакомится любой начинающий Android разработчик, это хранение в файле при помощи уже готового инструмента SharedPreferences.
Допустим, что нам нужно записывать имя пользователя и далее его отображать где-либо в приложении.
class UserStore(private val preferences: SharedPreferences) {
fun getUserName(): String? {
return preferences.getString(USER_NAME, "")
}
fun saveUserName(userName: String) {
preferences.edit().putString(USER_NAME, userName).apply()
}
companion object {
private const val USER_NAME = "user_name"
}
}Что такое делегат и как его готовят
В двух словах: это класс, который инкапсулирует установку и получение свойства. Кто хочет узнать большее - вот официальная документация.
Чтобы сделать класс делегатом, необходимо реализовать интерфейс ReadOnlyProperty для val и ReadWriteProperty для var. Передаем SharedPreferences ключ, по которому будет храниться свойство, и дефолтное значение через конструктор. В 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"
}
}Назначение свойству делегата осуществляется по ключевому слову by. Теперь каждый раз, когда данное свойство будет запрашиваться или устанавливаться, будут запускаться методы getValue и setValue созданного делегата.
Так же аналогично теперь можно поступить с другими полями, к примеру, если нужно так же сохранять телефон пользователя.
var userPhone: String by StringPreferencesDelegate(preferences, USER_PHONE, "")
Generic
Чтобы не делать для каждого типа данных отдельный делегат воспользуемся обобщениями (generics) – официальная документация.
Обычно первое неосознанное знакомство с 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 PreferencesDelegate<String?>(...)
Осталось сделать маппинг получения и установки свойств. Определяем тип данных по defaultValue, заодно это дает 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"
}
}Источник: Android preferences delegate