Skip to content

Instantly share code, notes, and snippets.

@rharter
Last active March 19, 2023 08:15
Show Gist options
  • Star 90 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save rharter/1df1cd72ce4e9d1801bd2d49f2a96810 to your computer and use it in GitHub Desktop.
Save rharter/1df1cd72ce4e9d1801bd2d49f2a96810 to your computer and use it in GitHub Desktop.
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)
}
@fonix232
Copy link

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

@yaroslavkulinich
Copy link

@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

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