f2prateek/rx-preferences

Default handling for Enums

Closed this issue · 3 comments

Currently an Enum Preference will throw if the string value stored in preference is not part of the enum. This can happen if a user installs a newer version of an app, changes the preference, then downgrades to a version before that enum option existed. Or can easily happen during development when introducing/removing/renaming enums. It's not straightforward for the app to handle in another way.
For my use, I'd like to see the default value returned for invalid enum values.

This isn't too straightforward to implement as the Preference.Adapter doesn't know what the default value is. One approach would be for Preference.get() returning the default value if adapter.get() returns null (or throwing something like an InvalidPreferenceException). I can work on this but wanted to post as an issue first as it might not be desirable functionality.

Can you use onErrorReturn to recover appropriately?

Sorry I just realized you were talking about get and not the RxJava API.

I think I'd still rather keep the existing behaviour and throw an error in this case. You can catch IllegalArgumentException thrown by Enum.valueOf (or if you're using RxJava - use one of these) to recover. The downside is you'd have to do this everywhere you wanted to handle the error (or wrap it in a helper function).

Preference<MyEnum> myEnum = rxPreferences.geEnum("myEnum", MyEnum.class);
try {
  myEnum.get();
} catch (IllegalArgumentException e) {
  // todo: Handle error.
}

Alternatively, you can also use a custom adapter that implements your custom behaviour. The advantage is that you can declare a fallback value (which can be the same as your default value if you want) in a single place.

Preference<MyEnum> myEnum = rxPreferences.getObject("myEnum", MyEnum.DEFAULT, new FallbackEnumAdapter(MyEnum.class, MyEnum.FALLBACK));
myEnum.get();
final class FallbackEnumAdapter <T extends Enum<T>> implements Preference.Adapter<T> {
  private final Class<T> enumClass;
  private final T fallbackValue;

  FallbackEnumAdapter(Class<T> enumClass, T fallbackValue) {
    this.enumClass = enumClass;
    this.fallbackValue = fallbackValue;
  }

  @Override public T get(@NonNull String key, @NonNull SharedPreferences preferences) {
    String value = preferences.getString(key, null);
    assert value != null; // Not called unless key is present.
    try {
      return Enum.valueOf(enumClass, value);
    } catch (IllegalArgumentException e) {
      return fallbackValue; 
    }
  }

  @Override
  public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) {
    editor.putString(key, value.name());
  }
}

Thanks, this works for me. If anyone else is interested, I'm using the above FallbackEnumAdapter with a little helper to create them:

public static <T extends Enum<T>> Preference<T> getEnum(RxSharedPreferences prefs, String key, T defaultValue) {
    return prefs.getObject(key, defaultValue, new FallbackEnumAdapter<>(defaultValue.getDeclaringClass(), defaultValue));
}

/* ... */
Preference<MyEnum> myEnumPreference = getEnum(rxPreferences, "myEnumKey", MyEnum.DEFAULT);