To increase the readability of calls to SharedPreferences.Editor I want to use a Kotlin variable that will execute 'getSharedPreferences.edit()' each time I need a new SharedPreferences.Editor. Initially I was going to use something like this:
val editPreferences: SharedPreferences.Editor = Application.getSharedPreferences("preferences", Context.MODE_PRIVATE).edit()
But then I was informed that 'editPreferences' will hold the reference to the same editor when what I really want it to create a new editor each time the 'editPreferences' is called.
If a custom getter was used would a new editor be returned each time? Something like this:
val editPreferences: SharedPreferences.Editor
get() = Application.getSharedPreferences("preferences", Context.MODE_PRIVATE).edit()
Still getting up and running with Kotlin and am not sure if the get() method would hold reference to the editor instead of creating a new one.
The second property declaration suits your needs: it has a custom getter, thus getting the property value will always execute the getter, and the value is not stored (the property has no backing field).
You are probably confused by equals sign in get() = ...
, but it is just a single-expression shorthand for the equivalent form of getter:
val editPreferences: SharedPreferences.Editor
get() {
return Application
.getSharedPreferences("preferences", Context.MODE_PRIVATE)
.edit()
}
If you implement a property with a custom getter, it will not store any data. The body of the getter will be executed every time you access the property.
You could go one step further and wrap the property with a delegate, making use of the variable name on the meta data.
Usage
class SomeActivity : SomeBaseActivity {
// Declare property the with key "myImportantNumber"
// and default value 10
var myImportantNumber by preference(10)
//how to access the property
fun isMyImportantNumberReallyHight() = myImportantNumber > 100
//how to edit the property
fun incrementMyImportantNumber(times:Int){
myImportantNumber = myImportantNumber + times
}
}
Delegation
The delegate keeps an instance of some preferences manager and uses the meta data on the property to fetch and save values on the shared preference.
class DelegatedPreference<T>(val default: T, val contextProvider:()-> Context) {
val manager by lazy { PreferencesManager(contextProvider()) }
@Suppress("UNCHECKED_CAST")
operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
return manager.get(prop.name, default)
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: Any) {
manager.save(prop.name, value)
}
class TypeNotImplementedException(val propName:String) : Exception("Type of ${propName} is not implemented on DelegatedPreference and thus invalid")
}
Some sugar
A little extension method:
fun <T> Activity.preference(default:T):DelegatedPreference<T>{
return DelegatedPreference(default, {this})
}
Which allow us to change this:
var myImportantNumber by DelegatedPreference(10, {this})
By something more readable:
var myImportantNumber by preference(10)
The actual getting and saving
Here what I called PreferencesManager
(sorry I didn't come up with a better name) does the heavy lifting, and calls .edit()
each time a property needs to be altered. It would look something like:
public class PreferencesManager(context: Context) {
private val preferences = getSharedPreferences(context)
companion object Utils {
public val APP_PREFERENCES: String = "APP_PREFERENCES"
fun getSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(APP_PREFERENCES, Context.MODE_PRIVATE)
}
}
public fun save(label:String, elem:Any){
when(elem){
is Int -> preferences.edit().putInt(label, elem).apply()
is String -> preferences.edit().putString(label, elem).apply()
is Float -> preferences.edit().putFloat(label, elem).apply()
is Boolean -> preferences.edit().putBoolean(label, elem).apply()
else -> throw DelegatedPreference.TypeNotImplementedException(label)
}
}
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
public fun <T> get(label:String, default:T):T = when(default){
is Int -> preferences.getInt(label, default)
is String -> preferences.getString(label, default)
is Float -> preferences.getFloat(label, default)
is Boolean -> preferences.getBoolean(label, default)
else -> throw DelegatedPreference.TypeNotImplementedException(label)
} as T
}
There is a lot of room for improvement here (like parametrizing the preference name instead of hardcoding it, giving an extension point for serializing other types, etc), but the general idea remains.
As hotkey and yole mentioned, a custom getter would cause a new editor to be returned each time, which is what you want.
However, please consider this extra-fancy solution that uses:
- inlining to reduce the overhead of calling the function
- extentions to make it seem like a method of the
Context
class even though we didn't define it
- higher-order functions to enable us to rid the to call
commit()
afterwards when using it
Declaration
inline fun Context.editPreferences(preferenceFileName:String = "preferences",block:SharedPreferences.Editor.() -> Unit)
{
val editablePreferences = getSharedPreferences(preferenceFileName,Context.MODE_PRIVATE).edit()
editablePreferences.block()
editablePreferences.commit()
}
Usage
Application.editPreferences()
{
putBoolean("SOME_BOOLEAN",true)
putFloat("SOME_FLOAT",293232F)
}
or in many cases, when the receiver is already a Context
you can do this:
editPreferences()
{
putBoolean("SOME_BOOLEAN",true)
putFloat("SOME_FLOAT",293232F)
}
Kotlin ❤