15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Kotlin らしく SharedPreferences をシンプルに扱う Extension を実装する (Moshi Json シリアライズ対応)

Last updated at Posted at 2018-11-03

20190925更新: compileSdkVersion 29 設定でビルドできるようにコードを修正しました。Enum のシリアライズに対応しました。

Kotlin の委譲プロパティを使って、Android SharedPreferences を無駄なくシンプルに扱えるようにします。

kotlinx.serialization 対応版は以下の記事を参照してください。

ライブラリを利用せず Extension だけで Kotlin らしく解決する方針です。

SharedPreferences へ簡単な構造化されたデータを保持したいときはモデルクラスを JSON 文字列化して保存する方法が一般的です。JSON 化に Moshi を使った委譲プロパティにも対応します。

プロパティとして利用できる型は以下の通りです。

  • Boolean
  • Float
  • Int
  • Long
  • String
  • Enum
  • Moshi JsonConverter 対応クラス

使い方

この Extension は次のように使用します。以下の点が大きなメリットとなります。

  • SharedPreferences の key 名に文字列定数を定義する必要がない
  • nullable、非 nullable を明確に使い分けられる
class FooConfiguration (private val preferences: SharedPreferences, moshi: Moshi)  {
    // 要素の存在確認
    val containsFoo: Boolean get() = preferences.contains(::foo.name)

    // key 名 "foo" での Int 値読み書き (他 boolean, float, int, long, string 対応)
    var foo: Int by preferences.int()

    // デフォルト値と保存キー名を指定するとき
    var foo2: Int by preferences.int(42, "my_foo_key")

    // key 名 "bar" での nullable Int 読み書き
    var bar: Int? by preferences.nullableInt()

    // key 名 "person" での JSON 対応クラス読み書き。デフォルト値に Person() を指定
    var person: Person by preferences.json(moshi, lazy { Person() })

    // 値が存在しないときに例外を発生させたいとき
    var person2: Person by preferences.json(moshi, lazy { throw Throwable() })  

    // nullable 版
    var person3: Person? by preferences.nullableJson(moshi)

    // key 名 "persons" で、任意の JsonAdapter に対応したいとき
    var persons: List<Person> by preferences.json(
        moshi.adapter(Types.newParameterizedType(List::class.java, Person::class.java),
        lazy { throw Throwable() })
}

SharedPreferencesExtension.kt

Extension の実装です。

import android.content.SharedPreferences
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
 * Boolean Read Write Delegate
 */
fun SharedPreferences.boolean(defaultValue: Boolean = false, key: String? = null): ReadWriteProperty<Any, Boolean> =
    delegate(defaultValue, key, SharedPreferences::getBoolean, SharedPreferences.Editor::putBoolean)

/**
 * Nullable Boolean Read Write Delegate
 */
fun SharedPreferences.nullableBoolean(key: String? = null): ReadWriteProperty<Any, Boolean?> =
    nullableDelegate(false, key, SharedPreferences::getBoolean, SharedPreferences.Editor::putBoolean)

/**
 * Float Read Write Delegate
 */
fun SharedPreferences.float(defaultValue: Float = 0f, key: String? = null): ReadWriteProperty<Any, Float> =
    delegate(defaultValue, key, SharedPreferences::getFloat, SharedPreferences.Editor::putFloat)

/**
 * Nullable Float Read Write Delegate
 */
fun SharedPreferences.nullableFloat(key: String? = null): ReadWriteProperty<Any, Float?> =
    nullableDelegate(0f, key, SharedPreferences::getFloat, SharedPreferences.Editor::putFloat)

/**
 * Int Read Write Delegate
 */
fun SharedPreferences.int(defaultValue: Int = 0, key: String? = null): ReadWriteProperty<Any, Int> =
    delegate(defaultValue, key, SharedPreferences::getInt, SharedPreferences.Editor::putInt)

/**
 * Nullable Int Read Write Delegate
 */
fun SharedPreferences.nullableInt(key: String? = null): ReadWriteProperty<Any, Int?> =
    nullableDelegate(0, key, SharedPreferences::getInt, SharedPreferences.Editor::putInt)

/**
 * Long Read Write Delegate
 */
fun SharedPreferences.long(defaultValue: Long = 0, key: String? = null): ReadWriteProperty<Any, Long> =
    delegate(defaultValue, key, SharedPreferences::getLong, SharedPreferences.Editor::putLong)

/**
 * Nullable Long Read Write Delegate
 */
fun SharedPreferences.nullableLong(key: String? = null): ReadWriteProperty<Any, Long?> =
    nullableDelegate(0, key, SharedPreferences::getLong, SharedPreferences.Editor::putLong)

/**
 * String Read Write Delegate
 */
fun SharedPreferences.string(defaultValue: String = "", key: String? = null): ReadWriteProperty<Any, String> =
    delegate(defaultValue, key, SharedPreferences::getString, SharedPreferences.Editor::putString)

/**
 * Nullable String Read Write Delegate
 */
fun SharedPreferences.nullableString(key: String? = null): ReadWriteProperty<Any, String?> =
    nullableDelegate("", key, SharedPreferences::getString, SharedPreferences.Editor::putString)

/**
 * Enum Read Write Delegate
 */
fun <T : Enum<T>> SharedPreferences.enum(valueOf: (String) -> T, defaultValue: Lazy<T>, key: String? = null) =
    object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty<*>): T {
            return getString(key ?: property.name, null)?.let { valueOf(it) } ?: defaultValue.value
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
            edit().putString(key ?: property.name, value.name).apply()
        }
    }

/**
 * Nullable Enum Read Write Delegate
 */
fun <T : Enum<T>> SharedPreferences.nullableEnum(valueOf: (String) -> T, key: String? = null) =
    object : ReadWriteProperty<Any, T?> {
        override fun getValue(thisRef: Any, property: KProperty<*>): T? {
            return getString(key ?: property.name, null)?.let { valueOf(it) }
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
            edit().putString(key ?: property.name, value?.name).apply()
        }
    }

/**
 * Moshi Json String Read Write Delegate
 */
inline fun <reified T : Any> SharedPreferences.json(
    moshi: Moshi,
    defaultValue: Lazy<T>,
    key: String? = null
): ReadWriteProperty<Any, T> =
    json(moshi.adapter(T::class.java), defaultValue, key)

/**
 * Nullable Moshi Json String Read Write Delegate
 */
inline fun <reified T : Any> SharedPreferences.nullableJson(
    moshi: Moshi,
    key: String? = null
): ReadWriteProperty<Any, T?> =
    nullableJson(moshi.adapter(T::class.java), key)

/**
 * Moshi Json String Read Write Delegate
 */
fun <T : Any> SharedPreferences.json(adapter: JsonAdapter<T>, defaultValue: Lazy<T>, key: String? = null) =
    object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty<*>): T {
            return getString(key ?: property.name, null)?.let(adapter::fromJson) ?: defaultValue.value
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
            edit().putString(key ?: property.name, adapter.toJson(value)).apply()
        }
    }

/**
 * Nullable Moshi Json String Read Write Delegate
 */
fun <T : Any> SharedPreferences.nullableJson(adapter: JsonAdapter<T>, key: String? = null) =
    object : ReadWriteProperty<Any, T?> {
        override fun getValue(thisRef: Any, property: KProperty<*>): T? {
            return getString(key ?: property.name, null)?.let(adapter::fromJson)
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
            edit().putString(key ?: property.name, value?.let(adapter::toJson)).apply()
        }
    }

private inline fun <T : Any> SharedPreferences.delegate(
    defaultValue: T, key: String?,
    crossinline getter: SharedPreferences.(key: String, defaultValue: T) -> T?,
    crossinline setter: SharedPreferences.Editor.(key: String, value: T) -> SharedPreferences.Editor
) = object : ReadWriteProperty<Any, T> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = getter(key ?: property.name, defaultValue) ?: defaultValue
    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) =
        edit().setter(key ?: property.name, value).apply()
}

private inline fun <T : Any> SharedPreferences.nullableDelegate(
    dummy: T, key: String?,
    crossinline getter: SharedPreferences.(key: String, defaultValue: T) -> T?,
    crossinline setter: SharedPreferences.Editor.(key: String, value: T) -> SharedPreferences.Editor
) = object : ReadWriteProperty<Any, T?> {
    override fun getValue(thisRef: Any, property: KProperty<*>): T? {
        val target = key ?: property.name
        return if (contains(target)) getter(target, dummy) else null
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
        val target = key ?: property.name
        if (value == null) {
            edit().remove(target).apply()
        } else {
            edit().setter(target, value).apply()
        }
    }
}

fun <T : Any> SharedPreferences.delegatefun <T : Any> SharedPreferences.nullableDelegate が ReadWriteProperty を返す本体です。それぞれ、呼び出すべき SharedPreferences の getter と setter を引数で受取り、適切に getValuesetValue を実装しています。

非 nullable の fun <T : Any> SharedPreferences.delegate は defaultValue を受取り、値が存在しなければ defaultValue を返すシンプルな実装。

nullable の fun <T : Any> SharedPreferences.nullableDelegate は、getter では key の存在チェックを挟んでから読み込むかどうかを決定し、setter では value が null であれば key を削除するというように null 値の扱いが特殊となります。

同様に、<T : Any> SharedPreferences.jsonfun <T : Any> SharedPreferences.nullableJson は getString, putString と Moshi JsonConverter を組み合わせて読み書きを実装します。putString は value = null を指定すると、指定した key の要素を削除する動作となるため、nullable の実装に分岐はありません。

Proguard について

Kotlin の KProperty は Proguard の影響を受けません。Proguard ありでビルドをしても KProperty::name で正しいプロパティ名を取得できていることは確認済みです。
この Extension 向けの Proguard 除外設定は必要ありません。

参考

以下の記事を参考にしました。

15
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?