Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Creates LiveData objects that observe a value in SharedPreferences while they have active listeners.
// 1. Get a reference to SharedPreferences however you normally would.
val prefs: SharedPreferences
// 2. Use the extension functions to create a LiveData object of whatever type you need and observe the result.
prefs.booleanLiveData("analytics_enabled", false).observe(this, { enabled ->
if (enabled != null && enabled) {
sendAnalytics()
}
})
import android.arch.lifecycle.LiveData
import android.content.SharedPreferences
abstract class SharedPreferenceLiveData<T>(val sharedPrefs: SharedPreferences,
val key: String,
val defValue: T) : LiveData<T>() {
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == this.key) {
value = getValueFromPreferences(key, defValue)
}
}
abstract fun getValueFromPreferences(key: String, defValue: T): T
override fun onActive() {
super.onActive()
value = getValueFromPreferences(key, defValue)
sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onInactive() {
sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onInactive()
}
}
class SharedPreferenceIntLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Int) :
SharedPreferenceLiveData<Int>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Int): Int = sharedPrefs.getInt(key, defValue)
}
class SharedPreferenceStringLiveData(sharedPrefs: SharedPreferences, key: String, defValue: String) :
SharedPreferenceLiveData<String>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: String): String = sharedPrefs.getString(key, defValue)
}
class SharedPreferenceBooleanLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Boolean) :
SharedPreferenceLiveData<Boolean>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean = sharedPrefs.getBoolean(key, defValue)
}
class SharedPreferenceFloatLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Float) :
SharedPreferenceLiveData<Float>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Float): Float = sharedPrefs.getFloat(key, defValue)
}
class SharedPreferenceLongLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Long) :
SharedPreferenceLiveData<Long>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Long): Long = sharedPrefs.getLong(key, defValue)
}
class SharedPreferenceStringSetLiveData(sharedPrefs: SharedPreferences, key: String, defValue: Set<String>) :
SharedPreferenceLiveData<Set<String>>(sharedPrefs, key, defValue) {
override fun getValueFromPreferences(key: String, defValue: Set<String>): Set<String> = sharedPrefs.getStringSet(key, defValue)
}
fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData<Int> {
return SharedPreferenceIntLiveData(this, key, defValue)
}
fun SharedPreferences.stringLiveData(key: String, defValue: String): SharedPreferenceLiveData<String> {
return SharedPreferenceStringLiveData(this, key, defValue)
}
fun SharedPreferences.booleanLiveData(key: String, defValue: Boolean): SharedPreferenceLiveData<Boolean> {
return SharedPreferenceBooleanLiveData(this, key, defValue)
}
fun SharedPreferences.floatLiveData(key: String, defValue: Float): SharedPreferenceLiveData<Float> {
return SharedPreferenceFloatLiveData(this, key, defValue)
}
fun SharedPreferences.longLiveData(key: String, defValue: Long): SharedPreferenceLiveData<Long> {
return SharedPreferenceLongLiveData(this, key, defValue)
}
fun SharedPreferences.stringSetLiveData(key: String, defValue: Set<String>): SharedPreferenceLiveData<Set<String>> {
return SharedPreferenceStringSetLiveData(this, key, defValue)
}
@magneticflux-
Copy link

magneticflux- commented Nov 21, 2017

@rharter You might consider adding a license to this. I assume you intended it to be under something like the MIT license or the Unlicense since the code is so small, but the default it All Rights Reserved.

@gajicm93
Copy link

gajicm93 commented Aug 9, 2018

Hi, this looks really great, and you saved me quite some time, but I'll propose 2 minor improvements:

  1. SharedPreferences.OnSharedPreferenceChangeListener is just an interface, not an abstract class, so you can implement it directly into SharedPreferenceLiveData, no need to use a separate val field for it, or I'm missing something?

  2. Also, this is more of a subjective preference, but why are you exposing getValueFromPreferences method, instead of just setting the initial value directly on the LiveData in the constructor? It would immediately emit the current value, instead of expecting the developer to explicitly request it, which would I believe simplify most implementations. Also, it's just a duplication of the LiveData internal functionality, since you can at any time call "getValue" on the LiveData directly.

@BhupeshSahu
Copy link

BhupeshSahu commented Oct 5, 2018

Hi gajicm93,
I've tried your first suggestion. It doesn't work when I tried to implement SharedPreferences.OnSharedPreferenceChangeListener on SharedPreferenceLiveData. It doesn't trigger event on the observer. I didn't get the apparent reason why it's not working. since I'm new to MVVM concept but just FYI.

@evbarnett
Copy link

evbarnett commented Oct 10, 2018

@gajicm93 / @BhupeshSahu

I think the reason for (1) is that SharedPreferences holds listeners in a WeakHashMap. So if you don't hold a real reference to your listener, it will be garbage collected and unable to be called.

See this answer

@idish
Copy link

idish commented Jan 30, 2019

Here's a java code version of this beautiful piece: https://gist.github.com/idish/f46a8327da7f293f943a5bda31078c95

@fplimapereira
Copy link

fplimapereira commented Jun 22, 2019

Hi! Could you please show an example of this code with mvvm pattern?

@fonix232
Copy link

fonix232 commented Nov 12, 2019

@fplimapereira MVVM has nothing to do with this bit of code.

@yaroslavkulinich
Copy link

yaroslavkulinich commented Mar 22, 2020

@rharter Thank you this good piece of code!
But I found some unwanted behavior using it in MVVM application.
So, in MVVM we use MediatorLiveData a lot, gathering different LiveDatas into one state.
When View layer component (Fragment or Activity) comes back to active state (after app switching for example), onActive() method of observed MediatorLiveData will try to trigger all of it's LiveDatas (only if LiveData changed, based on mVersion prop) onChanged() methods. If no changes - mediator does nothing. BUT, if we use your implementation of SharedPreferenceLiveData, composing MediatorLiveData, onChanged() will be triggered every time, as a result triggering mediator . That's because on every onActive() method you do value = getValueFromPreferences(key, defValue), that leads to mVersion increment -> and then to unwanted onChange() calls.
It will be much better to set initial value of SharedPreferenceLiveData during initialization, like :

init {
       value = this.getValueFromPreferences(key, defValue)
}

instead of doing it in onActive().

This approach will prevent "mediator triggering hell".

@lucassales2
Copy link

lucassales2 commented Aug 4, 2020

Shorter version using reifeid

inline fun <reified T> SharedPreferences.liveData(
    key: String,
    default: T
): SharedPreferenceLiveData<T> {
    @Suppress("UNCHECKED_CAST")
    return object : SharedPreferenceLiveData<T>(this, key, default) {
        override fun getValueFromPreferences(key: String, defValue: T): T {
            return when (default) {
                is String -> getString(key, default) as T
                is Int -> getInt(key, default) as T
                is Long -> getLong(key, default) as T
                is Boolean -> getBoolean(key, default) as T
                is Float -> getFloat(key, default) as T
                is Set<*> -> getStringSet(key, default as Set<String>) as T
                is MutableSet<*> -> getStringSet(key, default as MutableSet<String>) as T
                else -> throw IllegalArgumentException("generic type not handled")
            }
        }
    }
}

@TedHopp
Copy link

TedHopp commented Nov 27, 2020

This is a nice gist. My version below incorporates several changes.

  1. I replaced the abstract getValueFromPreferences method with a constructor function argument. The base class then doesn't need to be abstract or even open.
  2. onActive() and onInactive() in LiveData are just placeholders, so there's no need to call up to them.
  3. The solution suggested above by @yaroslavkulinich for what he called "mediator triggering hell" doesn't quite work in the case that the live data object is inactivated and then activated again. The preference value may have changed during the interim, so it's not enough to initialize at construction and then rely on listening for changes. I took a slightly different stab at the issue.
  4. SharedPreferences.getString and SharedPreferences.getStringSet return nullable values, so if the default value is not nullable, the code won't compile. Perhaps this was not an issue when the original gist was published.
  5. There's no need for a separate object to listen for preference changes. The class can implement the listener behavior directly.
  6. All the SharedPreferences extensions can share the same name. The compiler can figure out which function to use from the type of the default value. (I prefer overloading a single name, but it's a matter of style. Obviously, SharedPreferences itself doesn't use this approach.)
  7. I updated the import to use the JetPack version of LiveData.
import android.content.SharedPreferences
import androidx.lifecycle.LiveData

private class SharedPreferenceLiveData<T>(
    private val sharedPrefs: SharedPreferences,
    private val key: String,
    private val getPreferenceValue: () -> T,
) : LiveData<T>(getPreferenceValue()), SharedPreferences.OnSharedPreferenceChangeListener {
    override fun onActive() {
        sharedPrefs.registerOnSharedPreferenceChangeListener(this)
        updateIfChanged()
    }

    override fun onInactive() = sharedPrefs.unregisterOnSharedPreferenceChangeListener(this)

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
        if (key == this.key || key == null) {
            // Note that we get here on every preference write, even if the value has not changed
            updateIfChanged()
        }
    }

    /** Update the live data value, but only if the value has changed. */
    private fun updateIfChanged() = with(getPreferenceValue()) { if (value != this) value = this }
}

fun SharedPreferences.liveData(key: String, default: Int): LiveData<Int> =
    SharedPreferenceLiveData(this, key) { getInt(key, default) }

fun SharedPreferences.liveData(key: String, default: Long): LiveData<Long> =
    SharedPreferenceLiveData(this, key) { getLong(key, default) }

fun SharedPreferences.liveData(key: String, default: Boolean): LiveData<Boolean> =
    SharedPreferenceLiveData(this, key) { getBoolean(key, default) }

fun SharedPreferences.liveData(key: String, default: Float): LiveData<Float> =
    SharedPreferenceLiveData(this, key) { getFloat(key, default) }

fun SharedPreferences.liveData(key: String, default: String?): LiveData<String?> =
    SharedPreferenceLiveData(this, key) { getString(key, default) }

fun SharedPreferences.liveData(key: String, default: Set<String>?): LiveData<Set<String>?> =
    SharedPreferenceLiveData(this, key) { getStringSet(key, default) }

@rharter
Copy link
Author

rharter commented Nov 30, 2020

Nice updates!

@lucassales2, Does your reified solution handle nullability? i.e. string types can be nullable, so do you have to prefs.liveData("foo", null as String?) or something?

I see my original variant doesn't handle nullable types, either, but I don't think either of these can differentiate between String? and Set<String>?. (I'd personally be fine with defaulting Set<String> to an empty set in my projects, but that does change the behavior of the API.

@TedHopp
Copy link

TedHopp commented Nov 30, 2020

Yes, with my code a default value of null is ambiguous and prefs.liveData(key, null) won't compile. So you have to write null as String? or null as Set<String>? to let the compiler know what you want. That's the downside of overloading a single method name (and why I expect most people would prefer your original naming convention). 😞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment