Rx Preferences
Android's SharedPreferences offers a convenient mechanism to persist a collection of key-value pairs.
- Ask it for values with
get…
. - Save values with
put…
. - Listen for changes with
registerOnSharedPreferenceChangeListener
.
It's simplistic API makes it limiting for a few reasons:
- Callers must always know the preference key and type.
- No support for storing custom types out of the box.
- Callers cannot listen for changes to individual keys.
RxPreferences is a new(ish) library that builds on top of SharedPreferences
to solve these problems, and takes it further by integrating with RxJava.
Typed Preferences
SharedPreferences
requires callers to always know what key identifies a preference when they get or save a preference. Callers also need to keep track of what type was used for a preference (did the preference use a float
or int
), which can lead to subtle bugs.
SharedPreferences preferences = getDefaultSharedPreferences(this);
preferences.edit().putFloat("scale", 3.14f).commit();
// Circumvents the compiler and blows up at runtime.
preferences.getInt("scale", 0);
RxPreferences introduces a Preference
class, that identifies key used to store it and the type of data it holds, making it easier to spot such bugs at compile time. RxSharedPreferences
provides factory methods to promote preferences to objects.
SharedPreferences preferences = getDefaultSharedPreferences(this);
RxSharedPreferences rxPrefs = RxSharedPreferences.create(preferences);
Preference<Integer> foo = rxPrefs.getInt("foo");
foo.set(3.14f); // Will not compile!
The Preference
class provides methods that replace their counterparts in SharedPreferences
and SharedPreferences.Editor
. This makes it convenient to use them as the source of truth, instead of sharing String
constants throughout your app.
class Preference<T> {
// Equivalent to SharedPreferences.Editor#get….
T get();
// Equivalent to SharedPreferences#contains.
boolean isSet();
// Equivalent to SharedPreferences.Editor#put….
void set(T);
// Equivalent to SharedPreferences.Editor#remove.
void delete();
}
BYOA
SharedPreferences restricts you to a set of limited types — boolean
, float
, int
, long
, String
and Set<String>
. Trying to persist custom types is doable, but looks awkward.
@Inject SharedPreferences preferences;
// Gets unwieldy when repeated in 10 different places.
String serialized = preferences.getString("point", null);
if (serialized != null) {
Point point = Point.parse(serialized);
preferences.putString("point", point.toString());
}
RxPreferences introduces a pluggable Adapter
abstraction. An Adapter
can store and retrieve values of an arbitrary type, and consolidates your serialization logic into a single location.
public interface Adapter<T> {
T get(String key, SharedPreferences preferences);
void set(String key, T value, Editor editor);
}
RxPreferences provides built in adapters for all the types suppored by SharedPreferences
and enums. Writing a custom adapter is trivial. You can even use your own favorite serialization library!
class GsonPreferenceAdapter<T> implements Adapter<T> {
final Gson gson;
private Class<T> clazz;
// Constructor and exception handling omitted for brevity.
@Override
public T get(String key, SharedPreferences preferences) {
return gson.fromJson(preferences.getString(key), clazz);
}
@Override
public void set(String key, T value, Editor editor) {
editor.putString(key, gson.toJson(value));
}
}
Then, simply let RxPreferences know which adapter you want to use.
GsonPreferenceAdapter<Point> adapter
= new GsonPreferenceAdapter<>(gson, Point.class);
Preference<Point> pref = rxPrefs.getObject("point", null, adapter);
// Easy Peasy!
Point point = pref.get();
pref.set(point);
Reactive Bindings
OnSharedPreferenceChangeListener
requires that listeners observe changes to all keys. Callers must filter values for the keys they're interested in.
@Inject SharedPreferences prefs;
prefs.registerOnSharedPreferenceChangeListener((prefs, key) -> {
// This is a firehose of information!
// Ignore keys we aren't interested in.
if !FOO_KEY.equals(key) return;
boolean foo = prefs.getBoolean(key, false);
System.out.println(foo);
});
The Preference
class integrates with RxJava, and lets you observe changes to a single preference directly. Internally, RxPreferences shares a single listener amongst all Preference
objects to avoid unnecessary work.
@Inject @FooPreference BooleanPreference fooPreference;
fooPreference.asObservable()
.subscribe((enabled) -> System.out.println(enabled));
SharedPreferences+++
RxPreferences also lets you take actions on preferences to update or delete values. This makes it straightforward to set up complex pipelines by combining it with other libraries in the RxJava family.
For example, RxPreferences and RxBinding can be combined to hand roll your own simplified CheckBoxPreference
.
@Inject @LocationPreference BooleanPreference locationPreference;
@BindView(R.id.check_box) CheckBox checkBox;
// Update the checkbox when the preference changes.
locationPreference.asObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(RxCompoundButton.checked(checkBox));
// Update preference when the checkbox state changes.
RxCompoundButton.checkedChanges(checkBox)
.skip(1) // Skip the initial value.
.subscribe(locationPreference.asAction());
RxPreferences v1
RxPreferences makes it convenient to interact with SharedPreferences
, and integrating with RxJava makes it easy to express complex logic that would otherwise have been tedious and brittle. RxPreferences is available on Maven Central. Check the Github repo or u2020 to see more examples.
Happy persisting!
Thanks to Jake Wharton for polishing the API, and to Diana Smith for reading drafts of this post.