Search code examples
androidkotlinsharedpreferencesextension-function

Kotlin extension field for SharedPreference value by string constant key


Extension functions are great for the SharedPreference api in android. Jake Wharton has an interesting implementation at time code 32:30 of this video tutorial where he implements SharedPreferences extension function like so:

preferences.edit{
    set(USER_ID /*some string key constant somewhere*/, 42)
    //...
}

while this is ok, its kind of verbose.

This tutorial by Krupal Shah explains how you can reduce the getter/setter extension functions of SharedPreferences to:

preferences[USER_ID] = 42 
Log.i("User Id", preferences[USER_ID]) //User Id: 42    

This is pretty good, but the brackets imply iterable semantics, IMO. While not the worst thing in the world, you just wish that you could implement a field extension of a SharedPreferences value by the key constant itself.

My question is, is there any way to implement this type of extension on SharedPreferences?

preferences.USER_ID = 42
Log.i("User Id", preferences.USER_ID) //User Id: 42

Solution

  • First, let's create general interface for providing instance of SharedPreferences:

    interface SharedPreferencesProvider {
    
        val sharedPreferences: SharedPreferences
    }
    

    After we have to create delegate for property which will read/write value to preferences:

    object PreferencesDelegates {
    
        fun string(
            defaultValue: String = "",
            key: String? = null
        ): ReadWriteProperty<SharedPreferencesProvider, String> = 
            StringPreferencesProperty(defaultValue, key)
    }
    
    private class StringPreferencesProperty(
        private val defaultValue: String,
        private val key: String?
    ) : ReadWriteProperty<SharedPreferencesProvider, String> {
    
        override fun getValue(
             thisRef: SharedPreferencesProvider, 
             property: KProperty<*>
        ): String {
            val key = key ?: property.name
            return thisRef.sharedPreferences.getString(key, defaultValue)
        }
    
        override fun setValue(
            thisRef: SharedPreferencesProvider,
            property: KProperty<*>, 
            value: String
        ) {
            val key = key ?: property.name
            thisRef.sharedPreferences.save(key, value)
        }
    }
    

    PreferencesDelegates needed to hide implementation and add some readability to code. In the end it can be used like this:

    class AccountRepository(
        override val sharedPreferences: SharedPreferences
    ) : SharedPreferencesProvider {
    
        var currentUserId by PreferencesDelegates.string()
        var currentUserName by string() //With import
        var currentUserNickname by string(key = "CUSTOM_KEY", defaultValue = "Unknown")
    
        fun saveUser(id: String, name: String) {
            this.currentUserId = id
            this.currentUserName = name
        }
    }
    

    Similar can be implemented int, float or even custom type:

    open class CustomPreferencesProperty<T>(
        defaultValue: T,
        private val key: String?,
        private val getMapper: (String) -> T,
        private val setMapper: (T) -> String = { it.toString() }
    ) : ReadWriteProperty<SharedPreferencesProvider, T> {
    
        private val defaultValueRaw: String = setMapper(defaultValue)
    
        override fun getValue(
            thisRef: SharedPreferencesProvider, 
            property: KProperty<*>
        ): T {
            val key = property.name
            return getMapper(thisRef.sharedPreferences.getString(key, defaultValueRaw))
        }
    
        override fun setValue(
            thisRef: SharedPreferencesProvider, 
            property: KProperty<*>, 
            value: T
        ) {
            val key = property.name
            thisRef.sharedPreferences.save(key, setMapper(value))
        }
    }
    

    I wrote small library which covers such case. You can find rest of implemented preferences here

    EDIT. In case if you are using dagger:

    class AccountRepository @Injcet constructor() : SharedPreferencesProvider {
    
        @Inject
        override lateinit var sharedPreferences: SharedPreferences
    
        var currentUserId by PreferencesDelegates.string()
        ...
    
    }