Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[shared_preferences] Handling multiple files on Android #2040

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/shared_preferences/shared_preferences/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.0

* Possibility to specify the file under which the preferences are stored on Android.

## 0.5.4+7

* Restructure the project for Web support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,62 @@
@SuppressWarnings("unchecked")
class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {

private static final String SHARED_PREFERENCES_NAME = "FlutterSharedPreferences";
private static final String SHARED_PREFERENCES_DEFAULT_NAME = "FlutterSharedPreferences";
private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences";
private static final String PREFIX = "flutter.";

// Fun fact: The following is a base64 encoding of the string "This is the prefix for a list."
// Fun fact: The following is a base64 encoding of the string "This is the
// prefix for a list."
private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu";
private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy";
private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu";

private final android.content.SharedPreferences preferences;
private final Context context;
private final HashMap<String, SharedPreferences> instances;

/**
* Constructs a {@link MethodCallHandlerImpl} instance. Creates a {@link
* android.content.SharedPreferences} based on the {@code context}.
* Constructs a {@link MethodCallHandlerImpl} instance, and sets the {@link Context}. This should
* be used as a singleton. Use {@link #getPreferences} to get an instance of {@link
* SharedPreferences} associated to a specific file.
*/
MethodCallHandlerImpl(Context context) {
preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
this.context = context;
this.instances = new HashMap<>();
}

/**
* @param filename The file to store the preferences.
* @return An instance of {@link SharedPreferences}.
*/
private SharedPreferences getPreferences(String filename) {
if (filename == null) filename = SHARED_PREFERENCES_DEFAULT_NAME;
SharedPreferences instance = instances.get(filename);
if (instance == null) {
instance = context.getSharedPreferences(filename, Context.MODE_PRIVATE);
instances.put(filename, instance);
}
return instance;
}

@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
String key = call.argument("key");
final String key = call.argument("key");
final String filename = call.argument("filename");
final SharedPreferences preferences = getPreferences(filename);
try {
switch (call.method) {
case "setBool":
commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result);
break;
case "setDouble":
double doubleValue = ((Number) call.argument("value")).doubleValue();
String doubleValueStr = Double.toString(doubleValue);
final double doubleValue = ((Number) call.argument("value")).doubleValue();
final String doubleValueStr = Double.toString(doubleValue);
commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result);
break;
case "setInt":
Number number = call.argument("value");
final Number number = call.argument("value");
if (number instanceof BigInteger) {
BigInteger integerValue = (BigInteger) number;
final BigInteger integerValue = (BigInteger) number;
commitAsync(
preferences
.edit()
Expand All @@ -74,7 +96,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
}
break;
case "setString":
String value = (String) call.argument("value");
final String value = (String) call.argument("value");
if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) {
result.error(
"StorageError",
Expand All @@ -85,7 +107,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
commitAsync(preferences.edit().putString(key, value), result);
break;
case "setStringList":
List<String> list = call.argument("value");
final List<String> list = call.argument("value");
commitAsync(
preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result);
break;
Expand All @@ -94,14 +116,14 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
result.success(true);
break;
case "getAll":
result.success(getAllPrefs());
result.success(getAllPrefs(filename));
return;
case "remove":
commitAsync(preferences.edit().remove(key), result);
break;
case "clear":
Set<String> keySet = getAllPrefs().keySet();
SharedPreferences.Editor clearEditor = preferences.edit();
final Set<String> keySet = getAllPrefs(filename).keySet();
final SharedPreferences.Editor clearEditor = preferences.edit();
for (String keyToDelete : keySet) {
clearEditor.remove(keyToDelete);
}
Expand Down Expand Up @@ -161,36 +183,39 @@ private String encodeList(List<String> list) throws IOException {
}

// Filter preferences to only those set by the flutter app.
private Map<String, Object> getAllPrefs() throws IOException {
Map<String, ?> allPrefs = preferences.getAll();
Map<String, Object> filteredPrefs = new HashMap<>();
private Map<String, Object> getAllPrefs(String filename) throws IOException {
final SharedPreferences preferences = getPreferences(filename);
final Map<String, ?> allPrefs = preferences.getAll();
final Map<String, Object> filteredPrefs = new HashMap<>();
for (String key : allPrefs.keySet()) {
if (key.startsWith("flutter.")) {
if (key.startsWith(PREFIX)) {
Object value = allPrefs.get(key);
if (value instanceof String) {
String stringValue = (String) value;
final String stringValue = (String) value;
if (stringValue.startsWith(LIST_IDENTIFIER)) {
value = decodeList(stringValue.substring(LIST_IDENTIFIER.length()));
} else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) {
String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length());
final String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length());
value = new BigInteger(encoded, Character.MAX_RADIX);
} else if (stringValue.startsWith(DOUBLE_PREFIX)) {
String doubleStr = stringValue.substring(DOUBLE_PREFIX.length());
final String doubleStr = stringValue.substring(DOUBLE_PREFIX.length());
value = Double.valueOf(doubleStr);
}
} else if (value instanceof Set) {
// This only happens for previous usage of setStringSet. The app expects a list.
List<String> listValue = new ArrayList<>((Set) value);
final List<String> listValue = new ArrayList<>((Set) value);
// Let's migrate the value too while we are at it.
boolean success =
final boolean success =
preferences
.edit()
.remove(key)
.putString(key, LIST_IDENTIFIER + encodeList(listValue))
.commit();
if (!success) {
// If we are unable to migrate the existing preferences, it means we potentially lost them.
// In this case, an error from getAllPrefs() is appropriate since it will alert the app during plugin initialization.
// If we are unable to migrate the existing preferences, it means we potentially
// lost them.
// In this case, an error from getAllPrefs() is appropriate since it will alert
// the app during plugin initialization.
throw new IOException("Could not migrate set to list");
}
value = listValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,69 +23,118 @@ void main() {
'flutter.List': <String>['baz', 'quox'],
};

SharedPreferences preferences;
const String filename1 = "SharedPreferencesTests1";
const String filename2 = "SharedPreferencesTests2";

SharedPreferences preferences1;
SharedPreferences preferences2;

setUp(() async {
preferences = await SharedPreferences.getInstance();
preferences1 = await SharedPreferences.getInstance(filename: filename1);
preferences2 = await SharedPreferences.getInstance(filename: filename2);
});

tearDown(() {
preferences.clear();
preferences1.clear();
});

test('reading', () async {
expect(preferences.get('String'), isNull);
expect(preferences.get('bool'), isNull);
expect(preferences.get('int'), isNull);
expect(preferences.get('double'), isNull);
expect(preferences.get('List'), isNull);
expect(preferences.getString('String'), isNull);
expect(preferences.getBool('bool'), isNull);
expect(preferences.getInt('int'), isNull);
expect(preferences.getDouble('double'), isNull);
expect(preferences.getStringList('List'), isNull);
expect(preferences1.get('String'), isNull);
expect(preferences1.get('bool'), isNull);
expect(preferences1.get('int'), isNull);
expect(preferences1.get('double'), isNull);
expect(preferences1.get('List'), isNull);
expect(preferences1.getString('String'), isNull);
expect(preferences1.getBool('bool'), isNull);
expect(preferences1.getInt('int'), isNull);
expect(preferences1.getDouble('double'), isNull);
expect(preferences1.getStringList('List'), isNull);

expect(preferences2.get('String'), isNull);
expect(preferences2.get('bool'), isNull);
expect(preferences2.get('int'), isNull);
expect(preferences2.get('double'), isNull);
expect(preferences2.get('List'), isNull);
expect(preferences2.getString('String'), isNull);
expect(preferences2.getBool('bool'), isNull);
expect(preferences2.getInt('int'), isNull);
expect(preferences2.getDouble('double'), isNull);
expect(preferences2.getStringList('List'), isNull);
});

test('writing', () async {
await Future.wait(<Future<bool>>[
preferences.setString('String', kTestValues2['flutter.String']),
preferences.setBool('bool', kTestValues2['flutter.bool']),
preferences.setInt('int', kTestValues2['flutter.int']),
preferences.setDouble('double', kTestValues2['flutter.double']),
preferences.setStringList('List', kTestValues2['flutter.List'])
preferences1.setString('String', kTestValues2['flutter.String']),
preferences1.setBool('bool', kTestValues2['flutter.bool']),
preferences1.setInt('int', kTestValues2['flutter.int']),
preferences1.setDouble('double', kTestValues2['flutter.double']),
preferences1.setStringList('List', kTestValues2['flutter.List']),
preferences2.setString('String', kTestValues2['flutter.String']),
preferences2.setBool('bool', kTestValues2['flutter.bool']),
preferences2.setInt('int', kTestValues2['flutter.int']),
preferences2.setDouble('double', kTestValues2['flutter.double']),
preferences2.setStringList('List', kTestValues2['flutter.List'])
]);
expect(preferences.getString('String'), kTestValues2['flutter.String']);
expect(preferences.getBool('bool'), kTestValues2['flutter.bool']);
expect(preferences.getInt('int'), kTestValues2['flutter.int']);
expect(preferences.getDouble('double'), kTestValues2['flutter.double']);
expect(preferences.getStringList('List'), kTestValues2['flutter.List']);
expect(preferences1.getString('String'), kTestValues2['flutter.String']);
expect(preferences1.getBool('bool'), kTestValues2['flutter.bool']);
expect(preferences1.getInt('int'), kTestValues2['flutter.int']);
expect(preferences1.getDouble('double'), kTestValues2['flutter.double']);
expect(preferences1.getStringList('List'), kTestValues2['flutter.List']);

expect(preferences2.getString('String'), kTestValues2['flutter.String']);
expect(preferences2.getBool('bool'), kTestValues2['flutter.bool']);
expect(preferences2.getInt('int'), kTestValues2['flutter.int']);
expect(preferences2.getDouble('double'), kTestValues2['flutter.double']);
expect(preferences2.getStringList('List'), kTestValues2['flutter.List']);
});

test('removing', () async {
const String key = 'testKey';
preferences
preferences1
..setString(key, kTestValues['flutter.String'])
..setBool(key, kTestValues['flutter.bool'])
..setInt(key, kTestValues['flutter.int'])
..setDouble(key, kTestValues['flutter.double'])
..setStringList(key, kTestValues['flutter.List']);
await preferences.remove(key);
expect(preferences.get('testKey'), isNull);
await preferences1.remove(key);
expect(preferences1.get('testKey'), isNull);

preferences2
..setString(key, kTestValues['flutter.String'])
..setBool(key, kTestValues['flutter.bool'])
..setInt(key, kTestValues['flutter.int'])
..setDouble(key, kTestValues['flutter.double'])
..setStringList(key, kTestValues['flutter.List']);
await preferences2.remove(key);
expect(preferences2.get('testKey'), isNull);
});

test('clearing', () async {
preferences
preferences1
..setString('String', kTestValues['flutter.String'])
..setBool('bool', kTestValues['flutter.bool'])
..setInt('int', kTestValues['flutter.int'])
..setDouble('double', kTestValues['flutter.double'])
..setStringList('List', kTestValues['flutter.List']);
await preferences1.clear();
expect(preferences1.getString('String'), null);
expect(preferences1.getBool('bool'), null);
expect(preferences1.getInt('int'), null);
expect(preferences1.getDouble('double'), null);
expect(preferences1.getStringList('List'), null);

preferences2
..setString('String', kTestValues['flutter.String'])
..setBool('bool', kTestValues['flutter.bool'])
..setInt('int', kTestValues['flutter.int'])
..setDouble('double', kTestValues['flutter.double'])
..setStringList('List', kTestValues['flutter.List']);
await preferences.clear();
expect(preferences.getString('String'), null);
expect(preferences.getBool('bool'), null);
expect(preferences.getInt('int'), null);
expect(preferences.getDouble('double'), null);
expect(preferences.getStringList('List'), null);
await preferences2.clear();
expect(preferences2.getString('String'), null);
expect(preferences2.getBool('bool'), null);
expect(preferences2.getInt('int'), null);
expect(preferences2.getDouble('double'), null);
expect(preferences2.getStringList('List'), null);
});
});
}
Loading