1

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
flopshot
  • 1,153
  • 1
  • 16
  • 23

2 Answers2

2

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()
    ...

}
hluhovskyi
  • 9,556
  • 5
  • 30
  • 42
  • I love the OOP of implementing a SharedPrefsProvider interface. However, I was hoping to @Inject a singleton SharedPreferences instance (default) and call the extension field on that injected instance provided by dagger – flopshot Feb 05 '18 at 18:39
  • @flopshot you can do `override lateinit var sharedPreferences` and initalize it in the way you want – hluhovskyi Feb 05 '18 at 18:41
  • might I ask if you could elaborate in your answer? – flopshot Feb 05 '18 at 18:44
  • @flopshot I've added example – hluhovskyi Feb 05 '18 at 18:49
  • Thanks, just looking it over. Question, when the key is null (not specified in the delegate) it defaults in getValue/setValue to property.name. What does that mean? – flopshot Feb 05 '18 at 18:58
  • 1
    It means in case if you don't specify key for shared preferences explicitly it takes name of property instead. For example, if you write `var currentUserId by string()` it takes `currentUserId` as key for preference – hluhovskyi Feb 05 '18 at 19:01
  • Nice! Thanks again for the info. I'd like to tweak that null key default behavior a bit, but all in all great stuff. – flopshot Feb 05 '18 at 19:04
  • This saved my day. This is what I wanted to do. Defining multiple keys for different preferences were killing me. Thanks a lot for this approach :) – Alper Özaslan Jun 24 '22 at 16:04
0

You could define a simple extension property with a getter and a setter

var SharedPreferences.userId
    get() = getInt(USER_ID, 0)
    set(value: Int) { edit().putInt(USER_ID, value).apply() }
tynn
  • 38,113
  • 8
  • 108
  • 143
  • Thanks for the quick answer. I should have mentioned in the question that Jake and Krupal's implementation doesn't require the boiler plate of writing an extension for EVERY shared preference. I was hoping to keep that feature. – flopshot Feb 05 '18 at 18:41