The first logical step for Android™ support is to integrate with shared preferences because they change somewhat frequently, listening for changes is rarely done and not so straightforward, but can help make the app feel more reactive.

The most low-level way to bind a field to a preference is using a RawPreferenceField. The value is always nullable because the preference could be not present. It offers no option for a default value because it needs to mirror exactly the internal value of the preferences.

The functions to create it are extensions of the SharedPreferences class. As usual, for java they can be called statically in a special utility class. A complete list can be found here.

val sp = context.getSharedPreferences("test_prefs") //Obtain SharedPreferences instance val f /* : RawPreferenceField<String?> */ = sp.getRawStringField("pref_key") f.value = "hello" //Sets the preference to "hello" f.value = null //Removes the preference
SharedPreferences sp = getContext().getSharedPreferences("test_prefs"); //Obtain SharedPreferences instance RawPreferenceField<String> f = SharedPreferencesUtils.getRawStringField(sp, "pref_key"); f.setValue("hello"); //Sets the preference to "hello" f.setValue(null); //Removes the preference

The returned field is a special implementation of MutableField that will allow you to read and change the value of the shared preference. Setting it to null will remove the preference. It also extends the BasePreferenceField class that exposes these three properties:

Internally a listener is registered to the shared preferences so we can have multiple instances pointing at the same key without any problem. When the value is updated it is immediately applied.

Notice that the value mirrors exactly what is stored in the preferences: in this way we do not incur in problems related to the fields' laziness (i.e. the value doesn't change if it's the same).

Abstracting values


Preference fields can be created for all types supported by Android™'s shared preferences (Boolean, Int, Long, Float, String, Set<String>), but what if we want to store something a little more complex? Most of the times we store in the shared preferences a value that represents something but is not the whole object (like an ID). In this cases we can simply apply a two-way transformation, either explicitly by calling twoWayTransform() on the returned field or by passing the two functions directly to the get field function, like so:

val f /* : RawPreferenceField<Movie?> */ = sp.getRawLongField("favorite_movie", { Movie.byIdOrNull(it) }, { it.id }) f.value = avatar //Sets Avatar's ID to the preference f.value = null //Removes the preferece
SharedPreferences sp = getContext().getSharedPreferences("test_prefs"); //Obtain SharedPreferences instance RawPreferenceField<Movie> f = SharedPreferencesUtils.getRawLongField(sp, "favorite_movie", id -> Movie.byIdOrNull(id), movie -> movie.id); f.setValue(avatar); //Sets Avatar's ID to the preference f.setValue(null); //Removes the preference

The first function maps an low-level value to a high-level one, the second one is the opposite. In the example the first retrieves a movie from a saved Long ID, while the second one maps a movie to its ID that will be saved in the preferences.

Since null is used to represent the absence of the preference we cannot map it to a low-level value, however we are still able to return null in the low to high level one because we want the possibility to handle incorrect values. In the example above a movie with the saved ID could not be found for a lot of reasons. When this happens, the function yields null which then "pongs" back and removes the preference.