-
-
Save rharter/1df1cd72ce4e9d1801bd2d49f2a96810 to your computer and use it in GitHub Desktop.
// 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() | |
} | |
}) |
@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".
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")
}
}
}
}
This is a nice gist. My version below incorporates several changes.
- I replaced the abstract
getValueFromPreferences
method with a constructor function argument. The base class then doesn't need to be abstract or even open. onActive()
andonInactive()
inLiveData
are just placeholders, so there's no need to call up to them.- 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.
SharedPreferences.getString
andSharedPreferences.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.- There's no need for a separate object to listen for preference changes. The class can implement the listener behavior directly.
- 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.) - 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) }
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.
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). 😞
@fplimapereira MVVM has nothing to do with this bit of code.