はじめに
Delegated Properties を使って SharedPreferences を綺麗にしてみました。
(タイトルまんまですが..)
SharedPreferencesを使うクラスで保存する値が増えるたびに類似したコードが量産されていたので、
リファクタリングのタイミングで導入してみての使い勝手とコードを残します。
参考にしたサイトは以下です。
公式
Kotlinアンチパターン slide 66~
KotlinのReadWritePropertyを使ってAndroidのPreferenceをナウく書く
コードとユニットテスト
修正前
このまま、別のプロパティが追加されると増えるか、private メソッドを作るかでした...
class SharedPreferencesWrapper(private val context: Context) {
private var defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
var booleanKey: Boolean
get() = defaultPreferences.getBoolean("booleanKey", false)
set(value) = defaultPreferences.edit().putBoolean("booleanKey", value).apply()
var stringKey: String
get() = defaultPreferences.getString("stringKey", null) ?: ""
set(value) = defaultPreferences.edit().putString("stringKey", value).apply()
var intKey: Int
get() = defaultPreferences.getInt("intKey", 0)
set(value) = defaultPreferences.edit().putInt("intKey", value).apply()
var floatKey: Float
get() = defaultPreferences.getFloat("floatKey", 0f)
set(value) = defaultPreferences.edit().putFloat("floatKey", value).apply()
var longKey: Long
get() = defaultPreferences.getLong("longKey", 0L)
set(value) = defaultPreferences.edit().putLong("longKey", value).apply()
var stringSetKey: Set<String>
get() = defaultPreferences.getStringSet("stringSetKey", null) ?: hashSetOf()
set(value) = defaultPreferences.edit().putStringSet("stringSetKey", value).apply()
var dateKey: String
get() = defaultPreferences.getString("dateKey", "") ?: ""
set(value) = defaultPreferences.edit().putString("dateKey", value).apply()
var dataClassKey: Color?
get() {
val str = defaultPreferences.getString("dataClassKey", "")
str?.let {
return jacksonObjectMapper().readValue<Color>(it)
}
return null
}
set(value) {
val str = jacksonObjectMapper().writeValueAsString(value)
defaultPreferences.edit().putString("dataClassKey", str).apply()
}
var listKey: List<Color>?
get() {
val str = defaultPreferences.getString("listKey", "")
str?.let {
return jacksonObjectMapper().readValue(it)
}
return null
}
set(value) {
val str = jacksonObjectMapper().writeValueAsString(value)
defaultPreferences.edit().putString("listKey", str).apply()
}
}
data class Color(var name: String, var rgb:String)
Delegated Properties で 修正後
プロパティ の getter / setter を一行で定義できたので、
プロパティが増えてもクラス全体が極端に大きくなることはなさそうです。
(MutableSetなど元から少し型が変わっていますが..)
data class や list 構造などの場合、どこまで汎用的にするかですが、
数が少なければそのままでも良さそうです。
(今回、ObjectMapperにはJacksonを使いましたが、Gsonなど他のObjectMapperでも大丈夫です。)
class SharedPreferencesWrapperByDelegate(private val context: Context) {
private var defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
var booleanKey: Boolean by BooleanDelegate(defaultPreferences, "booleanKey", false)
var stringKey: String by StringDelegate(defaultPreferences, "stringKey", "")
var intKey: Int by IntDelegate(defaultPreferences, "intKey", 0)
var floatKey: Float by FloatDelegate(defaultPreferences, "floatKey", 0F)
var longKey: Long by LongDelegate(defaultPreferences, "longKey", 0L)
var stringSetKey: MutableSet<String> by SetDelegate(defaultPreferences, "stringSetKey", hashSetOf())
var dateKey: Date by DateDelegate(defaultPreferences, "stringSetKey", Date())
var dataClassKey: Color? by ColorDelegate(defaultPreferences, "dataClassKey", null)
var listKey: MutableList<Color>? by ListDelegate(defaultPreferences, "dataClassKey", null)
}
Delegated Properties のクラス
class BooleanDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Boolean
) : ReadWriteProperty<Any, Boolean> {
override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getBoolean(name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) =
sp.edit().putBoolean(name, value).apply()
}
class StringDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: String
) : ReadWriteProperty<Any, String> {
override fun getValue(thisRef: Any, property: KProperty<*>): String =
sp.getString(name, defaultValue) ?: ""
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) =
sp.edit().putString(name, value).apply()
}
class IntDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Int
) : ReadWriteProperty<Any, Int> {
override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getInt(name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) =
sp.edit().putInt(name, value).apply()
}
class FloatDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Float
) : ReadWriteProperty<Any, Float> {
override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getFloat(name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Float) =
sp.edit().putFloat(name, value).apply()
}
class LongDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Long
) : ReadWriteProperty<Any, Long> {
override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getLong(name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: Long) =
sp.edit().putLong(name, value).apply()
}
class SetDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: MutableSet<String>
) : ReadWriteProperty<Any, MutableSet<String>> {
override fun getValue(thisRef: Any, property: KProperty<*>): MutableSet<String> =
sp.getStringSet(name, defaultValue) ?: hashSetOf()
override fun setValue(thisRef: Any, property: KProperty<*>, value: MutableSet<String>) =
sp.edit().putStringSet(name, value).apply()
}
class DateDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Date
) : ReadWriteProperty<Any, Date> {
override fun getValue(thisRef: Any, property: KProperty<*>) = Date(sp.getLong(name, 0L))
override fun setValue(thisRef: Any, property: KProperty<*>, value: Date) =
sp.edit().putLong(name, value.time).apply()
}
class ColorDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: Color?
) : ReadWriteProperty<Any, Color?> {
override fun getValue(thisRef: Any, property: KProperty<*>): Color? {
val str = sp.getString(name, "") ?: ""
return try {
jacksonObjectMapper().readValue<Color>(str)
} catch (e: Exception) {
defaultValue
}
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: Color?) {
val str = jacksonObjectMapper().writeValueAsString(value)
sp.edit().putString(name, str).apply()
}
}
class ListDelegate(
private val sp: SharedPreferences,
private val name: String,
private val defaultValue: MutableList<Color>?
) : ReadWriteProperty<Any, MutableList<Color>?> {
override fun getValue(thisRef: Any, property: KProperty<*>): MutableList<Color>? {
val str = sp.getString(name, "") ?: ""
return try {
jacksonObjectMapper().readValue<MutableList<Color>>(str)
} catch (e: Exception) {
defaultValue
}
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: MutableList<Color>?) {
val str = jacksonObjectMapper().writeValueAsString(value)
sp.edit().putString(name, str).apply()
}
}
ユニットテスト
コード自体に対するユニットテストも書きました。
初期化、デフォルト値の検証をして、コードレベルの問題がないことを確認しています。
SharedPreferences をユニットテスト用にクラスを作って、
Contextがなくてもテスト対象のコードが実行出来るようにしています。
class DelegatesUnitTest {
@Test
fun test_BooleanDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.booleanKey, false)
wrapper.booleanKey = true
assertEquals(wrapper.booleanKey, true)
}
@Test
fun test_StringDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.stringKey, "")
wrapper.stringKey = "string"
assertEquals(wrapper.stringKey, "string")
}
@Test
fun test_IntDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.intKey, 0)
wrapper.intKey = 100
assertEquals(wrapper.intKey, 100)
}
@Test
fun test_FloatDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.floatKey, 0f)
wrapper.floatKey = 200f
assertEquals(wrapper.floatKey, 200f)
}
@Test
fun test_LongDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.longKey, 0L)
wrapper.longKey = 300L
assertEquals(wrapper.longKey, 300L)
}
@Test
fun test_StringSetDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertTrue(wrapper.stringSetKey.isEmpty())
val set = hashSetOf("one", "two")
wrapper.stringSetKey = set
assertTrue(wrapper.stringSetKey.size == 2)
assertTrue(wrapper.stringSetKey.contains("one"))
assertTrue(wrapper.stringSetKey.contains("two"))
}
@Test
fun test_DateDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertEquals(wrapper.dateKey, Date(0))
val current = Date()
wrapper.dateKey = current
assertEquals(wrapper.dateKey, current)
}
@Test
fun test_ObjectDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertNull(wrapper.dataClassKey)
wrapper.dataClassKey = Color("red", "ff0000")
assertEquals(wrapper.dataClassKey, Color("red", "ff0000"))
}
@Test
fun test_ListObjectDelegate() {
val wrapper = UnitTestSharedPreferencesWrapper()
assertNull(wrapper.listKey)
val list = arrayListOf<Color>(Color("red", "ff0000"), Color("green", "00ff00"))
wrapper.listKey = list
assertTrue(wrapper.listKey!!.size == 2)
assertTrue(wrapper.listKey!!.contains(Color("red", "ff0000")))
assertTrue(wrapper.listKey!!.contains(Color("green", "00ff00")))
}
}
class UnitTestSharedPreferencesWrapper() {
private val data = hashMapOf<String?, Any?>()
private var defaultPreferences = object : SharedPreferences {
override fun contains(p0: String?): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getBoolean(p0: String?, p1: Boolean): Boolean {
return data[p0] as? Boolean ?: p1
}
override fun unregisterOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getInt(p0: String?, p1: Int): Int {
return data[p0] as? Int ?: p1
}
override fun getAll(): MutableMap<String, *> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun edit(): SharedPreferences.Editor {
return object : SharedPreferences.Editor {
override fun clear(): SharedPreferences.Editor {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun putLong(p0: String?, p1: Long): SharedPreferences.Editor {
data[p0] = p1
return this
}
override fun putInt(p0: String?, p1: Int): SharedPreferences.Editor {
data[p0] = p1
return this
}
override fun remove(p0: String?): SharedPreferences.Editor {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun putBoolean(p0: String?, p1: Boolean): SharedPreferences.Editor {
data[p0] = p1
return this
}
override fun putStringSet(
p0: String?,
p1: MutableSet<String>?
): SharedPreferences.Editor {
data[p0] = p1
return this
}
override fun commit(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun putFloat(p0: String?, p1: Float): SharedPreferences.Editor {
data[p0] = p1
return this
}
override fun apply() {
}
override fun putString(p0: String?, p1: String?): SharedPreferences.Editor {
data[p0] = p1
return this
}
}
}
override fun getLong(p0: String?, p1: Long): Long {
return data[p0] as? Long ?: p1
}
override fun getFloat(p0: String?, p1: Float): Float {
return data[p0] as? Float ?: p1
}
override fun getStringSet(p0: String?, p1: MutableSet<String>?): MutableSet<String>? {
return data[p0] as? MutableSet<String> ?: p1
}
override fun registerOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getString(p0: String?, p1: String?): String? {
return data[p0] as? String ?: p1
}
}
var booleanKey: Boolean by BooleanDelegate(defaultPreferences, "booleanKey", false)
var stringKey: String by StringDelegate(defaultPreferences, "stringKey", "")
var intKey: Int by IntDelegate(defaultPreferences, "intKey", 0)
var floatKey: Float by FloatDelegate(defaultPreferences, "floatKey", 0F)
var longKey: Long by LongDelegate(defaultPreferences, "longKey", 0L)
var stringSetKey: MutableSet<String> by SetDelegate(defaultPreferences, "stringSetKey", mutableSetOf())
var dateKey: Date by DateDelegate(defaultPreferences, "dateKey", Date())
var dataClassKey: Color? by ColorDelegate(defaultPreferences, "dataClassKey", null)
var listKey: MutableList<Color>? by ListDelegate(defaultPreferences, "dataListClassKey", null)
}
まとめ
Delegated Properties は 考えて使えると、定型的な処理をかなり綺麗に依存できるかなと思いました。
ただ、ある程度使い方を限定しないとオレオレFWの世界に突入しそうでしたが...