Last active
September 19, 2024 22:18
-
-
Save threethan/6524aa69d75f65b46d59277c13f025a2 to your computer and use it in GitHub Desktop.
An easy to use Java-only wrapper for Android DataStores which can be used to migrate from SharedPreferences
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.SuppressLint; | |
import android.content.Context; | |
import android.content.SharedPreferences; | |
import android.os.Build; | |
import android.preference.PreferenceManager; | |
import android.util.Log; | |
import androidx.annotation.NonNull; | |
import androidx.annotation.Nullable; | |
import androidx.annotation.RequiresApi; | |
import androidx.datastore.migrations.SharedPreferencesView; | |
import androidx.datastore.preferences.core.MutablePreferences; | |
import androidx.datastore.preferences.core.Preferences; | |
import androidx.datastore.preferences.core.PreferencesKeys; | |
import androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder; | |
import androidx.datastore.rxjava3.RxDataStore; | |
import java.security.InvalidParameterException; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Set; | |
import io.reactivex.rxjava3.core.Single; | |
import io.reactivex.rxjava3.functions.Consumer; | |
// Add the following to build.gradle (app) dependencies: | |
// implementation 'androidx.datastore:datastore-preferences-rxjava3:1.0.0' | |
/** | |
* This class implements both SharedPreferences, and SharedPreferences.Editor | |
* <p> | |
* Most functions are implemented, and functions near-identically to their original implementations, | |
* but instead function using the newer {@link androidx.datastore.core.DataStore} in place of | |
* {@link SharedPreferences}. | |
* <p> | |
* In many cases, this may just be a drop-in replacement. However, keep in mind that writes are | |
* asynchronous by default, so async write followed by a read may produce unexpected results. | |
* You may set 'asyncWrite' to false at any time to get back synchronous behaviours. | |
* <p> | |
* Additional functions are provided for asynchronous reading and better handling of DataStores, | |
* which will be accessible when using this class directly, and make implementation of DataStore | |
* in Java much easier. | |
* @see android.content.SharedPreferences | |
* @see android.content.SharedPreferences.Editor | |
* @see androidx.datastore.core.DataStore | |
* @noinspection unused, UnusedReturnValue | |
* */ | |
public class DataStoreEditor implements SharedPreferences, SharedPreferences.Editor { | |
private static final String TAG = "DataStoreEditor"; | |
private static final Map<String, RxDataStore<Preferences>> dataStoreByName = new HashMap<>(); | |
/** If true, all write operations will be done asynchronously. | |
* If false, all write operations will be blocking. */ | |
public boolean asyncWrite = true; | |
RxDataStore<Preferences> dataStoreRX; | |
/** @noinspection rawtypes*/ | |
final static Class[] classes = new Class[]{ | |
String.class, Integer.class, Long.class, | |
Float.class, Double.class, Boolean.class, Set.class}; | |
/** | |
* Creates a new instance for the chosen DataStore of the given context | |
* @param context Context from which to get the DataStore | |
* @param name Name of the DataStore | |
*/ | |
public DataStoreEditor(Context context, String name) { | |
dataStoreRX = getDataStore(context, name); | |
} | |
/** | |
* Creates a new instance for the "default" DataStore of the given context | |
* @param context Context from which to get the DataStore | |
*/ | |
public DataStoreEditor(Context context) { | |
dataStoreRX = getDataStore(context, "default"); | |
} | |
synchronized private RxDataStore<Preferences> getDataStore(Context context, String name) { | |
if (dataStoreByName.containsKey(name)) return dataStoreByName.get(name); | |
RxDataStore<Preferences> ds = new RxPreferenceDataStoreBuilder(context, name).build(); | |
dataStoreByName.put(name, ds); | |
return ds; | |
} | |
/** | |
* Migrates a specific sharedPreference instance to this DataStore | |
* Data WILL be overridden! | |
* You should include your own mechanism to avoid running this more than once. | |
* @param sharedPreferences The sharedPreferences instance to migrate from | |
* <p> | |
* You may wish to set 'asyncWrite' to false before this operation. | |
*/ | |
@RequiresApi(api = Build.VERSION_CODES.N) | |
public void migrateFrom(SharedPreferences sharedPreferences) { | |
if (sharedPreferences instanceof DataStoreEditor) | |
throw new InvalidParameterException( | |
"Cannot migrate from another instance of DataStoreEditor"); | |
SharedPreferencesView sharedPreferencesView | |
= new SharedPreferencesView(sharedPreferences, sharedPreferences.getAll().keySet()); | |
Map<String, Object> allPrefs = sharedPreferencesView.getAll(); | |
allPrefs.forEach(this::putValue); | |
} | |
/** | |
* Migrates default sharedPreferences to this DataStore (calls migrateFrom) | |
* Data WILL be overridden! | |
* You should include your own mechanism to avoid running this more than once. | |
* You may wish to set 'asyncWrite' to false before this operation. | |
*/ | |
@RequiresApi(api = Build.VERSION_CODES.N) | |
public void migrateDefault(Context context) { | |
//noinspection deprecation | |
migrateFrom(PreferenceManager.getDefaultSharedPreferences(context)); | |
} | |
// Utility Functions | |
/** @noinspection unchecked | |
* Gets the key which matches the given type | |
* @param key Name of the key | |
* @param tClass Class of the value | |
* @return The value itself | |
* @param <T> Type of the value, should match tClass | |
*/ | |
public static <T> Preferences.Key<T> getKey(String key, Class<T> tClass) { | |
if (tClass == String.class ) return (Preferences.Key<T>) PreferencesKeys.stringKey (key); | |
if (tClass == Integer.class) return (Preferences.Key<T>) PreferencesKeys.intKey (key); | |
if (tClass == Long.class ) return (Preferences.Key<T>) PreferencesKeys.longKey (key); | |
if (tClass == Float.class ) return (Preferences.Key<T>) PreferencesKeys.floatKey (key); | |
if (tClass == Double.class ) return (Preferences.Key<T>) PreferencesKeys.doubleKey (key); | |
if (tClass == Boolean.class) return (Preferences.Key<T>) PreferencesKeys.booleanKey (key); | |
if (tClass == Set.class ) return (Preferences.Key<T>) PreferencesKeys.stringSetKey(key); | |
throw new InvalidParameterException("Invalid preference class: "+tClass+ | |
", must be one of "+ Arrays.toString(classes)); | |
} | |
/** @noinspection unchecked | |
* Gets the key which matches the type of the provided value | |
* @param key Name of the key | |
* @return The value itself | |
* @param <T> Type of the value, should be derived automatically from val | |
*/ | |
protected static <T> Preferences.Key<T> getKey(String key, @NonNull T val) { | |
if (val instanceof String ) return (Preferences.Key<T>) PreferencesKeys.stringKey (key); | |
if (val instanceof Integer) return (Preferences.Key<T>) PreferencesKeys.intKey (key); | |
if (val instanceof Long ) return (Preferences.Key<T>) PreferencesKeys.longKey (key); | |
if (val instanceof Float ) return (Preferences.Key<T>) PreferencesKeys.floatKey (key); | |
if (val instanceof Double ) return (Preferences.Key<T>) PreferencesKeys.doubleKey (key); | |
if (val instanceof Boolean) return (Preferences.Key<T>) PreferencesKeys.booleanKey (key); | |
if (val instanceof Set ) return (Preferences.Key<T>) PreferencesKeys.stringSetKey(key); | |
throw new InvalidParameterException("Invalid preference class, must be one of " | |
+ Arrays.toString(classes)); | |
} | |
/** | |
* Synchronously gets the value of the given key | |
* @param key The name of the key | |
* @param def The default value, if none was found | |
* @param tClass Class of the value | |
* @return The value at the given key | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> T getValue(String key, @Nullable T def, Class<T> tClass) { | |
Preferences.Key<T> prefKey = getKey(key, tClass); | |
T nullFallback = null; | |
@SuppressLint("UnsafeOptInUsageWarning") | |
Single<T> value = dataStoreRX.data().firstOrError() | |
.map(prefs -> prefs.get(prefKey)).onErrorReturn(throwable -> def); | |
return value.blockingGet(); | |
} | |
/** | |
* Synchronously gets the value of the given key | |
* @param key The name of the key | |
* @param def The default value, if none was found | |
* @return The value at the given key | |
* @param <T> Type of the value, should be derived automatically from def | |
*/ | |
public <T> T getValue(String key, @NonNull T def) { | |
Preferences.Key<T> prefKey = getKey(key, def); | |
T nullFallback = null; | |
@SuppressLint("UnsafeOptInUsageWarning") | |
Single<T> value = dataStoreRX.data().firstOrError() | |
.map(prefs -> prefs.get(prefKey)).onErrorReturn(throwable -> def); | |
return value.blockingGet(); | |
} | |
/** | |
* Asynchronously gets the value of the given key | |
* @param key The name of the key | |
* @param def The default value, if none was found | |
* @param tClass Class of the value | |
* @param consumer Consumer which will be called with the value when its ready | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> void getValue(String key, @Nullable T def, Consumer<T> consumer, Class<T> tClass) { | |
Preferences.Key<T> prefKey = getKey(key, tClass); | |
Single<T> value = dataStoreRX.data().firstOrError() | |
.map(prefs -> prefs.get(prefKey)).onErrorReturn(throwable -> def); | |
value.blockingSubscribe(consumer); | |
} | |
/** | |
* Asynchronously gets the value of the given key | |
* @param key The name of the key | |
* @param def The default value, if none was found | |
* @param consumer Consumer which will be called with the value when its ready | |
* @param <T> Type of the value, should be derived automatically from def | |
*/ | |
public <T> void getValue(String key, @NonNull T def, Consumer<T> consumer) { | |
Preferences.Key<T> prefKey = getKey(key, def); | |
Single<T> value = dataStoreRX.data().firstOrError() | |
.map(prefs -> prefs.get(prefKey)).onErrorReturn(throwable -> def); | |
value.blockingSubscribe(consumer); | |
} | |
/** | |
* Asynchronously writes a value which matches the given class | |
* @param key Name of key to write to | |
* @param value Value to write | |
* @param tClass Class of the value | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> void putValue(String key, @Nullable T value, Class<T> tClass) { | |
boolean returnvalue; | |
Preferences.Key<T> prefKey = getKey(key, tClass); | |
Single<Preferences> updateResult = dataStoreRX.updateDataAsync(prefsIn -> { | |
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); | |
mutablePreferences.set(prefKey, value); | |
return Single.just(mutablePreferences); | |
}); | |
} | |
/** | |
* Writes a value which matches the given class | |
* @param key Name of key to write to | |
* @param value Value to write | |
* @param tClass Class of the value | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> void putValue(String key, @Nullable T value, Class<T> tClass, boolean synchronous) { | |
boolean returnvalue; | |
Preferences.Key<T> prefKey = getKey(key, tClass); | |
Single<Preferences> updateResult = dataStoreRX.updateDataAsync(prefsIn -> { | |
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); | |
mutablePreferences.set(prefKey, value); | |
return Single.just(mutablePreferences); | |
}); | |
if (synchronous) { Preferences ignored = updateResult.blockingGet(); } | |
} | |
/** | |
* Asynchronously writes a value which matches the given class | |
* @param key Name of key to write to | |
* @param value Value to write | |
* @param <T> Type of the value, should be derived automatically from value | |
*/ | |
public <T> void putValue(String key, @NonNull T value) { | |
putValue(key, value, false); | |
} | |
/** | |
* Writes a value which matches the given class | |
* @param key Name of key to write to | |
* @param value Value to write | |
* @param <T> Type of the value, should be derived automatically from value | |
*/ | |
public <T> void putValue(String key, @NonNull T value, boolean synchronous) { | |
boolean returnvalue; | |
Preferences.Key<T> prefKey = getKey(key, value); | |
Single<Preferences> updateResult = dataStoreRX.updateDataAsync(prefsIn -> { | |
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); | |
mutablePreferences.set(prefKey, value); | |
return Single.just(mutablePreferences); | |
}); | |
if (synchronous) { Preferences ignored = updateResult.blockingGet(); } | |
} | |
/** | |
* Asynchronously removes a value which matches the given class | |
* @param key Name of the key to remove | |
* @param tClass Class of the value | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> void removeValue(String key, Class<T> tClass) { | |
removeValue(key, tClass, false); | |
} | |
/** | |
* Removes a value which matches the given class | |
* @param key Name of the key to remove | |
* @param tClass Class of the value | |
* @param <T> Type of the value, should be derived automatically from tClass | |
*/ | |
public <T> void removeValue(String key, Class<T> tClass, boolean synchronous){ | |
boolean returnvalue; | |
Preferences.Key<T> prefKey = getKey(key, tClass); | |
@SuppressLint("UnsafeOptInUsageWarning") | |
Single<Preferences> updateResult = dataStoreRX.updateDataAsync(prefsIn -> { | |
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); | |
T remove = mutablePreferences.remove(prefKey); | |
return Single.just(mutablePreferences); | |
}); | |
if (synchronous) { Preferences ignored = updateResult.blockingGet(); } | |
} | |
/** | |
* Asynchronously clears ALL data in the store | |
*/ | |
public DataStoreEditor clear(){ | |
boolean returnvalue; | |
@SuppressLint("UnsafeOptInUsageWarning") | |
Single<Preferences> updateResult = dataStoreRX.updateDataAsync(prefsIn -> { | |
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); | |
mutablePreferences.clear(); | |
return Single.just(mutablePreferences); | |
}); | |
return this; | |
} | |
// String | |
/** Synchronously reads and returns a value */ | |
public String getString(String key, String def) { | |
return getValue(key, def, String.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getString(String key, @NonNull String def, Consumer<String> consumer) { | |
getValue(key, def, consumer, String.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putString(String key, String value) { | |
putValue(key, value, String.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeString(String key) { | |
removeValue(key, String.class); | |
return this; | |
} | |
// Int | |
/** Synchronously reads and returns a value */ | |
public int getInt(String key, int def) { | |
return getValue(key, def, Integer.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getInt(String key, int def, Consumer<Integer> consumer) { | |
getValue(key, def, consumer, Integer.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putInt(String key, int value) { | |
putValue(key, value, Integer.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeInt(String key) { | |
removeValue(key, Integer.class); | |
return this; | |
} | |
// Long | |
/** Synchronously reads and returns a value */ | |
public long getLong(String key, long def) { | |
return getValue(key, def, Long.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getLong(String key, long def, Consumer<Long> consumer) { | |
getValue(key, def, consumer, Long.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putLong(String key, long value) { | |
putValue(key, value, Long.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeLong(String key) { | |
removeValue(key, Long.class); | |
return this; | |
} | |
// Float | |
/** Synchronously reads and returns a value */ | |
public float getFloat(String key, float def) { | |
return getValue(key, def, Float.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getFloat(String key, float def, Consumer<Float> consumer) { | |
getValue(key, def, consumer, Float.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putFloat(String key, float value) { | |
putValue(key, value, Float.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeFloat(String key) { | |
removeValue(key, Float.class); | |
return this; | |
} | |
// Double | |
/** Synchronously reads and returns a value */ | |
public double getDouble(String key, Double def) { | |
return getValue(key, def, Double.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getDouble(String key, double def, Consumer<Double> consumer) { | |
getValue(key, def, consumer, Double.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putDouble(String key, double value) { | |
putValue(key, value, Double.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeDouble(String key) { | |
removeValue(key, Double.class); | |
return this; | |
} | |
// Boolean | |
/** Synchronously reads and returns a value */ | |
public boolean getBoolean(String key, boolean def) { | |
return getValue(key, def, Boolean.class); | |
} | |
/** Asynchronously reads a value using a {@link Consumer} */ | |
public void getBoolean(String key, boolean def, Consumer<Boolean> consumer) { | |
getValue(key, def, consumer, Boolean.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putBoolean(String key, boolean value) { | |
putValue(key, value, Boolean.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeBoolean(String key) { | |
removeValue(key, Boolean.class); | |
return this; | |
} | |
// String Set | |
/** | |
* Synchronously fetches a string set. | |
* The set returned is made modifiable for consitency with the old sharedPreferences editor | |
*/ | |
public Set<String> getStringSet(String key, Set<String> def) { | |
//noinspection unchecked | |
return new HashSet<String>(getValue(key, def, Set.class)); | |
} | |
/** | |
* Asynchronously fetches a string set. | |
* The set returned is unmodifiable! | |
*/ | |
public void getStringSet(String key, @NonNull Set<String> def, Consumer<Set<String>> consumer) { | |
getValue(key, def, consumer::accept, Set.class); | |
} | |
/** Asynchronously writes a value */ | |
public DataStoreEditor putStringSet(String key, Set<String> value) { | |
putValue(key, value, Set.class); | |
return this; | |
} | |
/** Asynchronously removes a value */ | |
public DataStoreEditor removeStringSet(String key) { | |
removeValue(key, Set.class); | |
return this; | |
} | |
// Compat | |
/** | |
* This is included for compatiblity, but is slow and may not work as expected. | |
* <p> | |
* It's recommended to use typed remove functions instead. | |
* @noinspection rawtypes | |
*/ | |
@Deprecated | |
@Override | |
public DataStoreEditor remove(String key) { | |
for (Class tClass : classes) { | |
//noinspection unchecked | |
removeValue(key, tClass); | |
} | |
return this; | |
} | |
/*** This is included for compatiblity, but is slow! | |
* <p> | |
* It's recommented to instead try to read your specific typed value, | |
* you can use a default of null and check for a non-null return value. | |
*/ | |
@Override | |
public boolean contains(String key) { | |
//noinspection rawtypes | |
for (Class tClass : classes) { | |
//noinspection unchecked | |
if (getValue(key, null, tClass) != null) return true; | |
} | |
return false; | |
} | |
/** | |
* Commit/apply is no longer needed, and does nothing. | |
* Keep in mind that all write operations are performed asynchronously. | |
*/ | |
@Deprecated | |
public boolean commit() {return false;} | |
/** | |
* Commit/apply is no longer needed, and does nothing. | |
* Keep in mind that all write operations are performed asynchronously. | |
*/ | |
@Deprecated | |
public void apply() {} | |
// Compat (sharedpref) | |
/** | |
* This is included for compatiblity, but simply returns the same object, | |
* which implements both SharedPreferences and SharedPreferences.Editor | |
*/ | |
@Override | |
@Deprecated | |
public DataStoreEditor edit() { | |
return this; | |
} | |
/** | |
* Not implemented, will throw an error! | |
*/ | |
@Override | |
@Deprecated | |
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { | |
throw new RuntimeException("This function cannot be called on DataStoreEditor!"); | |
} | |
/** | |
* Not implemented, will throw an error! | |
*/ | |
@Override | |
@Deprecated | |
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { | |
throw new RuntimeException("This function cannot be called on DataStoreEditor!"); | |
} | |
/** | |
* Not implemented, will throw an error! | |
*/ | |
@Override | |
@Deprecated | |
public Map<String, ?> getAll() { | |
throw new RuntimeException("This function cannot be called on DataStoreEditor!"); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment