-
-
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() | |
} | |
}) |
Hi, this looks really great, and you saved me quite some time, but I'll propose 2 minor improvements:
-
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?
-
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.
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.
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.
Here's a java code version of this beautiful piece: https://gist.github.com/idish/f46a8327da7f293f943a5bda31078c95
Hi! Could you please show an example of this code with mvvm pattern?
@fplimapereira MVVM has nothing to do with this bit of code.
@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). 😞
@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.