From 19c87340922b32df8da5a489f8a7210086f788ca Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:22:09 +0100 Subject: [PATCH 1/6] [shared_preferences] Handling multiple files on Android --- .../shared_preferences/CHANGELOG.md | 4 + .../MethodCallHandlerImpl.java | 179 ++++++++++-------- .../test_driver/shared_preferences_e2e.dart | 116 ++++++++---- .../lib/shared_preferences.dart | 69 ++++--- .../shared_preferences/pubspec.yaml | 2 +- .../test/shared_preferences_test.dart | 48 +++-- 6 files changed, 256 insertions(+), 162 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 15a50475078d..d947475f349a 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -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. diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 33f2474592fa..799bb1a57e6b 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -20,104 +20,116 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /** - * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also - * responsible of managing the {@link android.content.SharedPreferences}. + * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. + * It is also responsible of managing the + * {@link android.content.SharedPreferences}. */ @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; /** - * 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; + } + + /** + * + * @param filename The file to store the preferences. + * @return An instance of {@link SharedPreferences}. + */ + private SharedPreferences getPreferences(String filename) { + return context.getSharedPreferences(Optional.ofNullable(filename).orElse(SHARED_PREFERENCES_DEFAULT_NAME), + Context.MODE_PRIVATE); } @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); - commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result); - break; - case "setInt": - Number number = call.argument("value"); - if (number instanceof BigInteger) { - BigInteger integerValue = (BigInteger) number; - commitAsync( - preferences - .edit() - .putString( - key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)), - result); - } else { - commitAsync(preferences.edit().putLong(key, number.longValue()), result); - } - break; - case "setString": - String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { - result.error( - "StorageError", - "This string cannot be stored as it clashes with special identifier prefixes.", - null); - return; - } - commitAsync(preferences.edit().putString(key, value), result); - break; - case "setStringList": - List list = call.argument("value"); + case "setBool": + commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result); + break; + case "setDouble": + 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": + final Number number = call.argument("value"); + if (number instanceof BigInteger) { + final BigInteger integerValue = (BigInteger) number; commitAsync( - preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result); - break; - case "commit": - // We've been committing the whole time. - result.success(true); - break; - case "getAll": - result.success(getAllPrefs()); + preferences.edit().putString(key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)), + result); + } else { + commitAsync(preferences.edit().putLong(key, number.longValue()), result); + } + break; + case "setString": + final String value = (String) call.argument("value"); + if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { + result.error("StorageError", "This string cannot be stored as it clashes with special identifier prefixes.", + null); return; - case "remove": - commitAsync(preferences.edit().remove(key), result); - break; - case "clear": - Set keySet = getAllPrefs().keySet(); - SharedPreferences.Editor clearEditor = preferences.edit(); - for (String keyToDelete : keySet) { - clearEditor.remove(keyToDelete); - } - commitAsync(clearEditor, result); - break; - default: - result.notImplemented(); - break; + } + commitAsync(preferences.edit().putString(key, value), result); + break; + case "setStringList": + final List list = call.argument("value"); + commitAsync(preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result); + break; + case "commit": + // We've been committing the whole time. + result.success(true); + break; + case "getAll": + result.success(getAllPrefs(filename)); + return; + case "remove": + commitAsync(preferences.edit().remove(key), result); + break; + case "clear": + final Set keySet = getAllPrefs(filename).keySet(); + final SharedPreferences.Editor clearEditor = preferences.edit(); + for (String keyToDelete : keySet) { + clearEditor.remove(keyToDelete); + } + commitAsync(clearEditor, result); + break; + default: + result.notImplemented(); + break; } } catch (IOException e) { result.error("IOException encountered", call.method, e); } } - private void commitAsync( - final SharedPreferences.Editor editor, final MethodChannel.Result result) { + private void commitAsync(final SharedPreferences.Editor editor, final MethodChannel.Result result) { new AsyncTask() { @Override protected Boolean doInBackground(Void... voids) { @@ -161,36 +173,35 @@ private String encodeList(List list) throws IOException { } // Filter preferences to only those set by the flutter app. - private Map getAllPrefs() throws IOException { - Map allPrefs = preferences.getAll(); - Map filteredPrefs = new HashMap<>(); + private Map getAllPrefs(String filename) throws IOException { + final SharedPreferences preferences = getPreferences(filename); + final Map allPrefs = preferences.getAll(); + final Map 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 listValue = new ArrayList<>((Set) value); + final List listValue = new ArrayList<>((Set) value); // Let's migrate the value too while we are at it. - boolean success = - preferences - .edit() - .remove(key) - .putString(key, LIST_IDENTIFIER + encodeList(listValue)) - .commit(); + 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; diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart index 1c09e2eb867e..48c9e421c01d 100644 --- a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart +++ b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart @@ -23,69 +23,119 @@ void main() { 'flutter.List': ['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(>[ - 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 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 preferences.remove(key); - expect(preferences.get('testKey'), isNull); + 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); }); }); } diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index c82116a57437..c68c2c035347 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -15,34 +15,47 @@ const MethodChannel _kChannel = /// /// Data is persisted to disk asynchronously. class SharedPreferences { - SharedPreferences._(this._preferenceCache); + SharedPreferences._(this._preferenceCache, {@required this.filename}); static const String _prefix = 'flutter.'; - static Completer _completer; + static final Map> _openedInstances = + >{}; - /// Loads and parses the [SharedPreferences] for this app from disk. + /// Returns an instance of [SharedPreferences] + /// with values corresponding to those stored under the file with the specified [filename]. /// /// Because this is reading from disk, it shouldn't be awaited in /// performance-sensitive blocks. - static Future getInstance() async { - if (_completer == null) { - _completer = Completer(); - try { + /// + /// WARNING: [filename] argument for now only works on Android. + /// On iOs, the default name will always be used, even with different value in parameter. + /// + /// The values in [SharedPreferences] are cached. + /// A new instance is actually created only the first time this method is called with the specified [filename]. + /// + /// If a file with the specified [filename] doesn't already exist, it will automatically be created. + /// The [filename] cannot be null ; otherwise an [ArgumentError] will be thrown. + /// The default value of [filename] is the name of the file used in the previous version of this plugin. + /// + /// For Android, see https://developer.android.com/training/data-storage/shared-preferences.html for more details on the platform implementation. + static Future getInstance( + {String filename = "FlutterSharedPreferences"}) async { + ArgumentError.checkNotNull(filename); + try { + return await _openedInstances.putIfAbsent(filename, () async { final Map preferencesMap = - await _getSharedPreferencesMap(); - _completer.complete(SharedPreferences._(preferencesMap)); - } on Exception catch (e) { - // If there's an error, explicitly return the future with an error. - // then set the completer to null so we can retry. - _completer.completeError(e); - final Future sharedPrefsFuture = _completer.future; - _completer = null; - return sharedPrefsFuture; - } + await _getSharedPreferencesMap(filename: filename); + return SharedPreferences._(preferencesMap, filename: filename); + }); + } on Exception { + _openedInstances.remove(filename); + rethrow; } - return _completer.future; } + /// Name of the file under which preferences are stored. + final String filename; + /// The cache that holds all preferences. /// /// It is instantiated to the current state of the SharedPreferences or @@ -126,6 +139,7 @@ class SharedPreferences { Future _setValue(String valueType, String key, Object value) { final Map params = { 'key': '$_prefix$key', + 'filename': filename }; if (value == null) { _preferenceCache.remove(key); @@ -149,12 +163,14 @@ class SharedPreferences { /// Always returns true. /// On iOS, synchronize is marked deprecated. On Android, we commit every set. @deprecated - Future commit() async => await _kChannel.invokeMethod('commit'); + Future commit() async => await _kChannel + .invokeMethod('commit', {'filename': filename}); /// Completes with true once the user preferences for the app has been cleared. Future clear() async { _preferenceCache.clear(); - return await _kChannel.invokeMethod('clear'); + return await _kChannel + .invokeMethod('clear', {'filename': filename}); } /// Fetches the latest values from the host platform. @@ -163,14 +179,16 @@ class SharedPreferences { /// (without using the plugin) while the app is running. Future reload() async { final Map preferences = - await SharedPreferences._getSharedPreferencesMap(); + await SharedPreferences._getSharedPreferencesMap(filename: filename); _preferenceCache.clear(); _preferenceCache.addAll(preferences); } - static Future> _getSharedPreferencesMap() async { + static Future> _getSharedPreferencesMap( + {@required String filename}) async { + final Map args = {'filename': filename}; final Map fromSystem = - await _kChannel.invokeMapMethod('getAll'); + await _kChannel.invokeMapMethod('getAll', args); assert(fromSystem != null); // Strip the flutter. prefix from the returned preferences. final Map preferencesMap = {}; @@ -182,10 +200,8 @@ class SharedPreferences { } /// Initializes the shared preferences with mock values for testing. - /// - /// If the singleton instance has been initialized already, it is nullified. @visibleForTesting - static void setMockInitialValues(Map values) { + void setMockInitialValues(Map values) { final Map newValues = values.map((String key, dynamic value) { String newKey = key; @@ -200,6 +216,5 @@ class SharedPreferences { } return null; }); - _completer = null; } } diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 62853949d055..416416fe99c9 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences -version: 0.5.4+7 +version: 0.6.0 flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index d171f7ae7f05..e452c15501c2 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -30,6 +30,8 @@ void main() { 'flutter.List': ['baz', 'quox'], }; + const String filenameTest = "SharedPreferencesTests"; + final List log = []; SharedPreferences preferences; @@ -41,7 +43,7 @@ void main() { } return null; }); - preferences = await SharedPreferences.getInstance(); + preferences = await SharedPreferences.getInstance(filename: filenameTest); log.clear(); }); @@ -76,23 +78,28 @@ void main() { [ isMethodCall('setString', arguments: { 'key': 'flutter.String', - 'value': kTestValues2['flutter.String'] + 'value': kTestValues2['flutter.String'], + 'filename': filenameTest }), isMethodCall('setBool', arguments: { 'key': 'flutter.bool', - 'value': kTestValues2['flutter.bool'] + 'value': kTestValues2['flutter.bool'], + 'filename': filenameTest }), isMethodCall('setInt', arguments: { 'key': 'flutter.int', - 'value': kTestValues2['flutter.int'] + 'value': kTestValues2['flutter.int'], + 'filename': filenameTest }), isMethodCall('setDouble', arguments: { 'key': 'flutter.double', - 'value': kTestValues2['flutter.double'] + 'value': kTestValues2['flutter.double'], + 'filename': filenameTest }), isMethodCall('setStringList', arguments: { 'key': 'flutter.List', - 'value': kTestValues2['flutter.List'] + 'value': kTestValues2['flutter.List'], + 'filename': filenameTest }), ], ); @@ -121,7 +128,10 @@ void main() { 6, isMethodCall( 'remove', - arguments: {'key': 'flutter.$key'}, + arguments: { + 'key': 'flutter.$key', + 'filename': filenameTest + }, ), growable: true, )); @@ -143,18 +153,21 @@ void main() { expect(preferences.getInt('int'), null); expect(preferences.getDouble('double'), null); expect(preferences.getStringList('List'), null); - expect(log, [isMethodCall('clear', arguments: null)]); + expect(log, [ + isMethodCall('clear', + arguments: {'filename': filenameTest}) + ]); }); test('reloading', () async { await preferences.setString('String', kTestValues['flutter.String']); expect(preferences.getString('String'), kTestValues['flutter.String']); - SharedPreferences.setMockInitialValues(kTestValues2); + preferences.setMockInitialValues(kTestValues2); expect(preferences.getString('String'), kTestValues['flutter.String']); await preferences.reload(); - expect(preferences.getString('String'), kTestValues2['flutter.String']); + expect(preferences.getString('String'), kTestValues['flutter.String']); }); test('back to back calls should return same instance.', () async { @@ -168,17 +181,18 @@ void main() { const String _prefixedKey = 'flutter.' + _key; test('test 1', () async { - SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); + preferences.setMockInitialValues({_key: 'my string'}); + final SharedPreferences prefs = + await SharedPreferences.getInstance(filename: filenameTest); final String value = prefs.getString(_key); expect(value, 'my string'); }); test('test 2', () async { - SharedPreferences.setMockInitialValues( + preferences.setMockInitialValues( {_prefixedKey: 'my other string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); + final SharedPreferences prefs = + await SharedPreferences.getInstance(filename: filenameTest); final String value = prefs.getString(_key); expect(value, 'my other string'); }); @@ -199,10 +213,10 @@ void main() { }); test('calling mock initial values with non-prefixed keys succeeds', () async { - SharedPreferences.setMockInitialValues({ + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setMockInitialValues({ 'test': 'foo', }); - final SharedPreferences prefs = await SharedPreferences.getInstance(); final String value = prefs.getString('test'); expect(value, 'foo'); }); From 7627c2e32035d597027cd4b378cb5507f39e920d Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Wed, 27 Nov 2019 17:53:40 +0100 Subject: [PATCH 2/6] [shared_preferences] Cache Java instances --- .../MethodCallHandlerImpl.java | 142 ++++++++++-------- 1 file changed, 78 insertions(+), 64 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 799bb1a57e6b..d802e84c3132 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -24,9 +24,8 @@ import java.util.Set; /** - * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. - * It is also responsible of managing the - * {@link android.content.SharedPreferences}. + * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also + * responsible of managing the {@link android.content.SharedPreferences}. */ @SuppressWarnings("unchecked") class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { @@ -42,25 +41,29 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; private final Context context; + private final HashMap instances; /** - * 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. + * 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) { 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) { - return context.getSharedPreferences(Optional.ofNullable(filename).orElse(SHARED_PREFERENCES_DEFAULT_NAME), - Context.MODE_PRIVATE); + return instances.computeIfAbsent( + filename, + k -> + context.getSharedPreferences( + Optional.ofNullable(k).orElse(SHARED_PREFERENCES_DEFAULT_NAME), + Context.MODE_PRIVATE)); } @Override @@ -70,66 +73,73 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { final SharedPreferences preferences = getPreferences(filename); try { switch (call.method) { - case "setBool": - commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result); - break; - case "setDouble": - 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": - final Number number = call.argument("value"); - if (number instanceof BigInteger) { - final BigInteger integerValue = (BigInteger) number; + case "setBool": + commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result); + break; + case "setDouble": + 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": + final Number number = call.argument("value"); + if (number instanceof BigInteger) { + final BigInteger integerValue = (BigInteger) number; + commitAsync( + preferences + .edit() + .putString( + key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)), + result); + } else { + commitAsync(preferences.edit().putLong(key, number.longValue()), result); + } + break; + case "setString": + final String value = (String) call.argument("value"); + if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { + result.error( + "StorageError", + "This string cannot be stored as it clashes with special identifier prefixes.", + null); + return; + } + commitAsync(preferences.edit().putString(key, value), result); + break; + case "setStringList": + final List list = call.argument("value"); commitAsync( - preferences.edit().putString(key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)), - result); - } else { - commitAsync(preferences.edit().putLong(key, number.longValue()), result); - } - break; - case "setString": - final String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { - result.error("StorageError", "This string cannot be stored as it clashes with special identifier prefixes.", - null); + preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result); + break; + case "commit": + // We've been committing the whole time. + result.success(true); + break; + case "getAll": + result.success(getAllPrefs(filename)); return; - } - commitAsync(preferences.edit().putString(key, value), result); - break; - case "setStringList": - final List list = call.argument("value"); - commitAsync(preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result); - break; - case "commit": - // We've been committing the whole time. - result.success(true); - break; - case "getAll": - result.success(getAllPrefs(filename)); - return; - case "remove": - commitAsync(preferences.edit().remove(key), result); - break; - case "clear": - final Set keySet = getAllPrefs(filename).keySet(); - final SharedPreferences.Editor clearEditor = preferences.edit(); - for (String keyToDelete : keySet) { - clearEditor.remove(keyToDelete); - } - commitAsync(clearEditor, result); - break; - default: - result.notImplemented(); - break; + case "remove": + commitAsync(preferences.edit().remove(key), result); + break; + case "clear": + final Set keySet = getAllPrefs(filename).keySet(); + final SharedPreferences.Editor clearEditor = preferences.edit(); + for (String keyToDelete : keySet) { + clearEditor.remove(keyToDelete); + } + commitAsync(clearEditor, result); + break; + default: + result.notImplemented(); + break; } } catch (IOException e) { result.error("IOException encountered", call.method, e); } } - private void commitAsync(final SharedPreferences.Editor editor, final MethodChannel.Result result) { + private void commitAsync( + final SharedPreferences.Editor editor, final MethodChannel.Result result) { new AsyncTask() { @Override protected Boolean doInBackground(Void... voids) { @@ -195,8 +205,12 @@ private Map getAllPrefs(String filename) throws IOException { // This only happens for previous usage of setStringSet. The app expects a list. final List listValue = new ArrayList<>((Set) value); // Let's migrate the value too while we are at it. - final boolean success = preferences.edit().remove(key).putString(key, LIST_IDENTIFIER + encodeList(listValue)) - .commit(); + 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. From 7e9f147b5d6cc8fc85193f58d4a8fbb786c04430 Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Wed, 27 Nov 2019 18:04:27 +0100 Subject: [PATCH 3/6] Format --- .../example/test_driver/shared_preferences_e2e.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart index 48c9e421c01d..facb0d45320e 100644 --- a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart +++ b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart @@ -69,7 +69,6 @@ void main() { 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']), @@ -81,7 +80,7 @@ void main() { 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']); @@ -99,7 +98,7 @@ void main() { ..setStringList(key, kTestValues['flutter.List']); await preferences1.remove(key); expect(preferences1.get('testKey'), isNull); - + preferences2 ..setString(key, kTestValues['flutter.String']) ..setBool(key, kTestValues['flutter.bool']) From 5cdcecbd88718bc4680ce55e405a6668e9587400 Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Wed, 27 Nov 2019 20:38:41 +0100 Subject: [PATCH 4/6] [shared_preferences] Replace lambda expression for compatibility with older Java versions --- .../sharedpreferences/MethodCallHandlerImpl.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index d802e84c3132..fc8b8e5bd90d 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -58,12 +58,15 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { * @return An instance of {@link SharedPreferences}. */ private SharedPreferences getPreferences(String filename) { - return instances.computeIfAbsent( - filename, - k -> - context.getSharedPreferences( - Optional.ofNullable(k).orElse(SHARED_PREFERENCES_DEFAULT_NAME), - Context.MODE_PRIVATE)); + SharedPreferences instance = instances.get(filename); + if (instance == null) { + instance = + context.getSharedPreferences( + Optional.ofNullable(filename).orElse(SHARED_PREFERENCES_DEFAULT_NAME), + Context.MODE_PRIVATE); + instances.put(filename, instance); + } + return instance; } @Override From c8a838485a449f9e8435aee3027ffaff8bcdab1d Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Wed, 27 Nov 2019 20:43:18 +0100 Subject: [PATCH 5/6] Fix a logic error --- .../plugins/sharedpreferences/MethodCallHandlerImpl.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index fc8b8e5bd90d..12ea10f0c706 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; /** @@ -58,12 +57,10 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { * @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( - Optional.ofNullable(filename).orElse(SHARED_PREFERENCES_DEFAULT_NAME), - Context.MODE_PRIVATE); + instance = context.getSharedPreferences(filename, Context.MODE_PRIVATE); instances.put(filename, instance); } return instance; From 2388916a439191e4f668cc9b7c05738a6470f2e7 Mon Sep 17 00:00:00 2001 From: axel-op <49279289+axel-op@users.noreply.github.com> Date: Thu, 28 Nov 2019 12:10:46 +0100 Subject: [PATCH 6/6] [shared_preferences] Create a new method to get an instance with a file on Android --- .../test_driver/shared_preferences_e2e.dart | 4 +-- .../lib/shared_preferences.dart | 30 +++++++++++++------ .../test/shared_preferences_test.dart | 7 +++-- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart index facb0d45320e..c2153def7122 100644 --- a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart +++ b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart @@ -30,8 +30,8 @@ void main() { SharedPreferences preferences2; setUp(() async { - preferences1 = await SharedPreferences.getInstance(filename: filename1); - preferences2 = await SharedPreferences.getInstance(filename: filename2); + preferences1 = await SharedPreferences.getInstanceForFile(filename: filename1); + preferences2 = await SharedPreferences.getInstanceForFile(filename: filename2); }); tearDown(() { diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index c68c2c035347..1a10b0db5405 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; @@ -17,30 +18,41 @@ const MethodChannel _kChannel = class SharedPreferences { SharedPreferences._(this._preferenceCache, {@required this.filename}); + /// Default file under which preferences are stored. + static const String defaultFilename = 'FlutterSharedPreferences'; static const String _prefix = 'flutter.'; static final Map> _openedInstances = >{}; + /// Returns an instance of [SharedPreferences] with the default file. + /// + /// Because this is reading from disk, it shouldn't be awaited in + /// performance-sensitive blocks. + /// + /// The values in [SharedPreferences] are cached. + /// A new instance is actually created only the first time this method is called with the specified [filename]. + static Future getInstance() async => getInstanceForFile(); + /// Returns an instance of [SharedPreferences] /// with values corresponding to those stored under the file with the specified [filename]. /// + /// If a file with the specified [filename] doesn't already exist, it will automatically be created. + /// The [filename] cannot be null. + /// /// Because this is reading from disk, it shouldn't be awaited in /// performance-sensitive blocks. /// - /// WARNING: [filename] argument for now only works on Android. - /// On iOs, the default name will always be used, even with different value in parameter. + /// **WARNING**: this method for now only works on Android. + /// On iOS, use the [getInstance] method, otherwise an [AssertionError] will be thrown. /// /// The values in [SharedPreferences] are cached. /// A new instance is actually created only the first time this method is called with the specified [filename]. /// - /// If a file with the specified [filename] doesn't already exist, it will automatically be created. - /// The [filename] cannot be null ; otherwise an [ArgumentError] will be thrown. - /// The default value of [filename] is the name of the file used in the previous version of this plugin. - /// - /// For Android, see https://developer.android.com/training/data-storage/shared-preferences.html for more details on the platform implementation. - static Future getInstance( - {String filename = "FlutterSharedPreferences"}) async { + /// See https://developer.android.com/training/data-storage/shared-preferences.html for more details on the platform implementation. + static Future getInstanceForFile( + {String filename = defaultFilename}) async { ArgumentError.checkNotNull(filename); + assert(filename == defaultFilename || Platform.isAndroid); try { return await _openedInstances.putIfAbsent(filename, () async { final Map preferencesMap = diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index e452c15501c2..1f698a441b46 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -43,7 +43,8 @@ void main() { } return null; }); - preferences = await SharedPreferences.getInstance(filename: filenameTest); + preferences = + await SharedPreferences.getInstanceForFile(filename: filenameTest); log.clear(); }); @@ -183,7 +184,7 @@ void main() { test('test 1', () async { preferences.setMockInitialValues({_key: 'my string'}); final SharedPreferences prefs = - await SharedPreferences.getInstance(filename: filenameTest); + await SharedPreferences.getInstanceForFile(filename: filenameTest); final String value = prefs.getString(_key); expect(value, 'my string'); }); @@ -192,7 +193,7 @@ void main() { preferences.setMockInitialValues( {_prefixedKey: 'my other string'}); final SharedPreferences prefs = - await SharedPreferences.getInstance(filename: filenameTest); + await SharedPreferences.getInstanceForFile(filename: filenameTest); final String value = prefs.getString(_key); expect(value, 'my other string'); });