diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index adefa4f2b8c..044aedfec29 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Adds `getAllWithPrefix` and `clearWithPrefix` methods. + ## 2.0.17 * Clarifies explanation of endorsement in README. diff --git a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index cea3f34b9b9..7bdc8e7239c 100644 --- a/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -106,16 +106,18 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { // We've been committing the whole time. result.success(true); break; - case "getAll": - result.success(getAllPrefs()); + case "getAllWithPrefix": + String prefix = call.argument("prefix"); + result.success(getAllPrefs(prefix)); return; case "remove": commitAsync(preferences.edit().remove(key), result); break; - case "clear": - Set keySet = getAllPrefs().keySet(); + case "clearWithPrefix": + String newPrefix = call.argument("prefix"); + Set keys = getAllPrefs(newPrefix).keySet(); SharedPreferences.Editor clearEditor = preferences.edit(); - for (String keyToDelete : keySet) { + for (String keyToDelete : keys) { clearEditor.remove(keyToDelete); } commitAsync(clearEditor, result); @@ -181,12 +183,12 @@ private String encodeList(List list) throws IOException { } } - // Filter preferences to only those set by the flutter app. - private Map getAllPrefs() throws IOException { + // Gets all shared preferences, filtered to only those set with the given prefix. + private Map getAllPrefs(String prefix) throws IOException { Map allPrefs = preferences.getAll(); 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; diff --git a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart index 4d4a85a5fcb..46ce5e48e87 100644 --- a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart @@ -11,22 +11,36 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesAndroid', () { - const Map kTestValues = { + const Map flutterTestValues = { 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], }; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], }; + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + late SharedPreferencesStorePlatform preferences; setUp(() async { @@ -34,95 +48,227 @@ void main() { }); tearDown(() { - preferences.clear(); + preferences.clearWithPrefix(''); }); - // Normally the app-facing package adds the prefix, but since this test - // bypasses the app-facing package it needs to be manually added. - String prefixedKey(String key) { - return 'flutter.$key'; - } - testWidgets('reading', (WidgetTester _) async { - final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], isNull); - expect(values[prefixedKey('bool')], isNull); - expect(values[prefixedKey('int')], isNull); - expect(values[prefixedKey('double')], isNull); - expect(values[prefixedKey('List')], isNull); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], isNull); + expect(values['Bool'], isNull); + expect(values['Int'], isNull); + expect(values['Double'], isNull); + expect(values['StringList'], isNull); }); - testWidgets('writing', (WidgetTester _) async { + testWidgets('getAllWithPrefix', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', prefixedKey('String'), kTestValues2['flutter.String']!), + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), preferences.setValue( - 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), preferences.setValue( - 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), + 'String', 'flutter.String', allTestValues['flutter.String']!), preferences.setValue( - 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), preferences.setValue( - 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], allTestValues['prefix.String']); + expect(values['prefix.Bool'], allTestValues['prefix.Bool']); + expect(values['prefix.Int'], allTestValues['prefix.Int']); + expect(values['prefix.Double'], allTestValues['prefix.Double']); + expect(values['prefix.StringList'], allTestValues['prefix.StringList']); + }); + + testWidgets('clearWithPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), + preferences.setValue( + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix('prefix.'); + Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], null); + expect(values['prefix.Bool'], null); + expect(values['prefix.Int'], null); + expect(values['prefix.Double'], null); + expect(values['prefix.StringList'], null); + values = await preferences.getAllWithPrefix('flutter.'); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('getAllWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], allTestValues['String']); + expect(values['Bool'], allTestValues['Bool']); + expect(values['Int'], allTestValues['Int']); + expect(values['Double'], allTestValues['Double']); + expect(values['StringList'], allTestValues['StringList']); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('clearWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix(''); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], null); + expect(values['Bool'], null); + expect(values['Int'], null); + expect(values['Double'], null); + expect(values['StringList'], null); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); + }); + + testWidgets('getAll', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) ]); final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[prefixedKey('List')], kTestValues2['flutter.List']); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); }); - testWidgets('removing', (WidgetTester _) async { - final String key = prefixedKey('testKey'); - await preferences.setValue('String', key, kTestValues['flutter.String']!); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); - await preferences.setValue('Int', key, kTestValues['flutter.int']!); - await preferences.setValue('Double', key, kTestValues['flutter.double']!); + testWidgets('remove', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setValue( + 'String', key, allTestValues['flutter.String']!); + await preferences.setValue('Bool', key, allTestValues['flutter.Bool']!); + await preferences.setValue('Int', key, allTestValues['flutter.Int']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']!); + 'Double', key, allTestValues['flutter.Double']!); + await preferences.setValue( + 'StringList', key, allTestValues['flutter.StringList']!); await preferences.remove(key); - final Map values = await preferences.getAll(); + final Map values = await preferences.getAllWithPrefix(''); expect(values[key], isNull); }); - testWidgets('clearing', (WidgetTester _) async { + testWidgets('clear', (WidgetTester _) async { + await preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!); await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']!); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']!); + 'Int', 'flutter.Int', allTestValues['flutter.Int']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']!); + 'Double', 'flutter.Double', allTestValues['flutter.Double']!); + await preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!); await preferences.clear(); final Map values = await preferences.getAll(); - expect(values['String'], null); - expect(values['bool'], null); - expect(values['int'], null); - expect(values['double'], null); - expect(values['List'], null); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); }); testWidgets('simultaneous writes', (WidgetTester _) async { final List> writes = >[]; const int writeCount = 100; for (int i = 1; i <= writeCount; i++) { - writes.add(preferences.setValue('Int', prefixedKey('int'), i)); + writes.add(preferences.setValue('Int', 'Int', i)); } final List result = await Future.wait(writes, eagerError: true); // All writes should succeed. expect(result.where((bool element) => !element), isEmpty); // The last write should win. - final Map values = await preferences.getAll(); - expect(values[prefixedKey('int')], writeCount); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['Int'], writeCount); }); testWidgets('string clash with lists, big integers and doubles', (WidgetTester _) async { - final String key = prefixedKey('akey'); + const String key = 'akey'; const String value = 'a string value'; - await preferences.clear(); + await preferences.clearWithPrefix(''); // Special prefixes used to store datatypes that can't be stored directly // in SharedPreferences as strings instead. @@ -137,7 +283,8 @@ void main() { for (final String prefix in specialPrefixes) { expect(preferences.setValue('String', key, prefix + value), throwsA(isA())); - final Map values = await preferences.getAll(); + final Map values = + await preferences.getAllWithPrefix(''); expect(values[key], null); } }); diff --git a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart index da5147d32da..b77db6c14cc 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart @@ -19,6 +19,8 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { SharedPreferencesStorePlatform.instance = SharedPreferencesAndroid(); } + static const String _defaultPrefix = 'flutter.'; + @override Future remove(String key) async { return (await _kChannel.invokeMethod( @@ -37,17 +39,28 @@ class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { @override Future clear() async { - return (await _kChannel.invokeMethod('clear'))!; + return clearWithPrefix(_defaultPrefix); + } + + @override + Future clearWithPrefix(String prefix) async { + return (await _kChannel.invokeMethod( + 'clearWithPrefix', + {'prefix': prefix}, + ))!; } @override Future> getAll() async { - final Map? preferences = - await _kChannel.invokeMapMethod('getAll'); + return getAllWithPrefix(_defaultPrefix); + } - if (preferences == null) { - return {}; - } - return preferences; + @override + Future> getAllWithPrefix(String prefix) async { + return (await _kChannel.invokeMapMethod( + 'getAllWithPrefix', + {'prefix': prefix}, + )) ?? + {}; } } diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 963ed8bc00b..02dd43abf95 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.17 +version: 2.1.0 environment: sdk: ">=2.17.0 <4.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - shared_preferences_platform_interface: ^2.0.0 + shared_preferences_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart index f1043daac1a..9c4ce2111d7 100644 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart @@ -16,15 +16,36 @@ void main() { 'plugins.flutter.io/shared_preferences_android', ); - const Map kTestValues = { + const Map flutterTestValues = { 'flutter.String': 'hello world', 'flutter.Bool': true, 'flutter.Int': 42, 'flutter.Double': 3.14159, 'flutter.StringList': ['foo', 'bar'], }; - // Create a dummy in-memory implementation to back the mocked method channel - // API to simplify validation of the expected calls. + + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + late InMemorySharedPreferencesStore testData; final List log = []; @@ -45,6 +66,12 @@ void main() { if (methodCall.method == 'getAll') { return testData.getAll(); } + if (methodCall.method == 'getAllWithPrefix') { + final Map arguments = + getArgumentDictionary(methodCall); + final String prefix = arguments['prefix']! as String; + return testData.getAllWithPrefix(prefix); + } if (methodCall.method == 'remove') { final Map arguments = getArgumentDictionary(methodCall); @@ -54,6 +81,12 @@ void main() { if (methodCall.method == 'clear') { return testData.clear(); } + if (methodCall.method == 'clearWithPrefix') { + final Map arguments = + getArgumentDictionary(methodCall); + final String prefix = arguments['prefix']! as String; + return testData.clearWithPrefix(prefix); + } final RegExp setterRegExp = RegExp(r'set(.*)'); final Match? match = setterRegExp.matchAsPrefix(methodCall.method); if (match?.groupCount == 1) { @@ -77,14 +110,21 @@ void main() { test('getAll', () async { store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await store.getAll(), kTestValues); - expect(log.single.method, 'getAll'); + testData = InMemorySharedPreferencesStore.withData(allTestValues); + expect(await store.getAll(), flutterTestValues); + expect(log.single.method, 'getAllWithPrefix'); + }); + + test('getAllWithPrefix', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(allTestValues); + expect(await store.getAllWithPrefix('prefix.'), prefixTestValues); + expect(log.single.method, 'getAllWithPrefix'); }); test('remove', () async { store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); + testData = InMemorySharedPreferencesStore.withData(allTestValues); expect(await store.remove('flutter.String'), true); expect(await store.remove('flutter.Bool'), true); expect(await store.remove('flutter.Int'), true); @@ -102,13 +142,13 @@ void main() { test('setValue', () async { store = SharedPreferencesAndroid(); expect(await testData.getAll(), isEmpty); - for (final String key in kTestValues.keys) { - final Object value = kTestValues[key]!; + for (final String key in allTestValues.keys) { + final Object value = allTestValues[key]!; expect(await store.setValue(key.split('.').last, key, value), true); } - expect(await testData.getAll(), kTestValues); + expect(await testData.getAll(), flutterTestValues); - expect(log, hasLength(5)); + expect(log, hasLength(15)); expect(log[0].method, 'setString'); expect(log[1].method, 'setBool'); expect(log[2].method, 'setInt'); @@ -118,11 +158,36 @@ void main() { test('clear', () async { store = SharedPreferencesAndroid(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); + testData = InMemorySharedPreferencesStore.withData(allTestValues); expect(await testData.getAll(), isNotEmpty); expect(await store.clear(), true); expect(await testData.getAll(), isEmpty); - expect(log.single.method, 'clear'); + expect(log.single.method, 'clearWithPrefix'); + }); + + test('clearWithPrefix', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(allTestValues); + + expect(await testData.getAllWithPrefix('prefix.'), isNotEmpty); + expect(await store.clearWithPrefix('prefix.'), true); + expect(await testData.getAllWithPrefix('prefix.'), isEmpty); + }); + + test('getAllWithNoPrefix', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(allTestValues); + + expect(await testData.getAllWithPrefix(''), hasLength(15)); + }); + + test('clearWithNoPrefix', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(allTestValues); + + expect(await testData.getAllWithPrefix(''), isNotEmpty); + expect(await store.clearWithPrefix(''), true); + expect(await testData.getAllWithPrefix(''), isEmpty); }); }); } diff --git a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md index 06d7f957456..d5a6fa27043 100644 --- a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Adds `getAllWithPrefix` and `clearWithPrefix` methods. + ## 2.1.5 * Clarifies explanation of endorsement in README. diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift index c97698ce0f7..b334f68d83d 100644 --- a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift @@ -22,8 +22,8 @@ public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi { UserDefaultsApiSetup.setUp(binaryMessenger: messenger, api: instance) } - func getAll() -> [String? : Any?] { - return getAllPrefs(); + func getAllWithPrefix(prefix: String) -> [String? : Any?] { + return getAllPrefs(prefix: prefix) } func setBool(key: String, value: Bool) { @@ -42,23 +42,23 @@ public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi { UserDefaults.standard.removeObject(forKey: key) } - func clear() { + func clearWithPrefix(prefix: String) { let defaults = UserDefaults.standard - for (key, _) in getAllPrefs() { + for (key, _) in getAllPrefs(prefix: prefix) { defaults.removeObject(forKey: key) } } -} -/// Returns all preferences stored by this plugin. -private func getAllPrefs() -> [String: Any] { - var filteredPrefs: [String: Any] = [:] - if let appDomain = Bundle.main.bundleIdentifier, - let prefs = UserDefaults.standard.persistentDomain(forName: appDomain) - { - for (key, value) in prefs where key.hasPrefix("flutter.") { - filteredPrefs[key] = value + /// Returns all preferences stored with specified prefix. + func getAllPrefs(prefix: String) -> [String: Any] { + var filteredPrefs: [String: Any] = [:] + if let appDomain = Bundle.main.bundleIdentifier, + let prefs = UserDefaults.standard.persistentDomain(forName: appDomain) + { + for (key, value) in prefs where key.hasPrefix(prefix) { + filteredPrefs[key] = value + } } + return filteredPrefs } - return filteredPrefs } diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift index 933217b7bf9..57a08e8de0f 100644 --- a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v5.0.0), do not edit directly. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -14,15 +14,33 @@ import FlutterMacOS #endif -/// Generated class from Pigeon. + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)" + ] +} /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UserDefaultsApi { - func remove(key: String) - func setBool(key: String, value: Bool) - func setDouble(key: String, value: Double) - func setValue(key: String, value: Any) - func getAll() -> [String?: Any?] - func clear() + func remove(key: String) throws + func setBool(key: String, value: Bool) throws + func setDouble(key: String, value: Double) throws + func setValue(key: String, value: Any) throws + func getAllWithPrefix(prefix: String) throws -> [String?: Any?] + func clearWithPrefix(prefix: String) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -33,10 +51,14 @@ class UserDefaultsApiSetup { let removeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.remove", binaryMessenger: binaryMessenger) if let api = api { removeChannel.setMessageHandler { message, reply in - let args = message as! [Any?] + let args = message as! [Any] let keyArg = args[0] as! String - api.remove(key: keyArg) - reply(wrapResult(nil)) + do { + try api.remove(key: keyArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } } } else { removeChannel.setMessageHandler(nil) @@ -44,11 +66,15 @@ class UserDefaultsApiSetup { let setBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setBool", binaryMessenger: binaryMessenger) if let api = api { setBoolChannel.setMessageHandler { message, reply in - let args = message as! [Any?] + let args = message as! [Any] let keyArg = args[0] as! String let valueArg = args[1] as! Bool - api.setBool(key: keyArg, value: valueArg) - reply(wrapResult(nil)) + do { + try api.setBool(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } } } else { setBoolChannel.setMessageHandler(nil) @@ -56,11 +82,15 @@ class UserDefaultsApiSetup { let setDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setDouble", binaryMessenger: binaryMessenger) if let api = api { setDoubleChannel.setMessageHandler { message, reply in - let args = message as! [Any?] + let args = message as! [Any] let keyArg = args[0] as! String let valueArg = args[1] as! Double - api.setDouble(key: keyArg, value: valueArg) - reply(wrapResult(nil)) + do { + try api.setDouble(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } } } else { setDoubleChannel.setMessageHandler(nil) @@ -68,44 +98,48 @@ class UserDefaultsApiSetup { let setValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setValue", binaryMessenger: binaryMessenger) if let api = api { setValueChannel.setMessageHandler { message, reply in - let args = message as! [Any?] + let args = message as! [Any] let keyArg = args[0] as! String - let valueArg = args[1]! - api.setValue(key: keyArg, value: valueArg) - reply(wrapResult(nil)) + let valueArg = args[1] + do { + try api.setValue(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } } } else { setValueChannel.setMessageHandler(nil) } - let getAllChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.getAll", binaryMessenger: binaryMessenger) + let getAllWithPrefixChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.getAllWithPrefix", binaryMessenger: binaryMessenger) if let api = api { - getAllChannel.setMessageHandler { _, reply in - let result = api.getAll() - reply(wrapResult(result)) + getAllWithPrefixChannel.setMessageHandler { message, reply in + let args = message as! [Any] + let prefixArg = args[0] as! String + do { + let result = try api.getAllWithPrefix(prefix: prefixArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } } } else { - getAllChannel.setMessageHandler(nil) + getAllWithPrefixChannel.setMessageHandler(nil) } - let clearChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.clear", binaryMessenger: binaryMessenger) + let clearWithPrefixChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.clearWithPrefix", binaryMessenger: binaryMessenger) if let api = api { - clearChannel.setMessageHandler { _, reply in - api.clear() - reply(wrapResult(nil)) + clearWithPrefixChannel.setMessageHandler { message, reply in + let args = message as! [Any] + let prefixArg = args[0] as! String + do { + try api.clearWithPrefix(prefix: prefixArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } } } else { - clearChannel.setMessageHandler(nil) + clearWithPrefixChannel.setMessageHandler(nil) } } } - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: FlutterError) -> [Any?] { - return [ - error.code, - error.message, - error.details - ] -} diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift index a4dd4b58f92..c33b99014ab 100644 --- a/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift @@ -13,52 +13,61 @@ import FlutterMacOS @testable import shared_preferences_foundation class RunnerTests: XCTestCase { + let prefixes: [String] = ["aPrefix", ""] + func testSetAndGet() throws { - let plugin = SharedPreferencesPlugin() + for aPrefix in prefixes { + let plugin = SharedPreferencesPlugin() - plugin.setBool(key: "flutter.aBool", value: true) - plugin.setDouble(key: "flutter.aDouble", value: 3.14) - plugin.setValue(key: "flutter.anInt", value: 42) - plugin.setValue(key: "flutter.aString", value: "hello world") - plugin.setValue(key: "flutter.aStringList", value: ["hello", "world"]) + plugin.setBool(key: "\(aPrefix)aBool", value: true) + plugin.setDouble(key: "\(aPrefix)aDouble", value: 3.14) + plugin.setValue(key: "\(aPrefix)anInt", value: 42) + plugin.setValue(key: "\(aPrefix)aString", value: "hello world") + plugin.setValue(key: "\(aPrefix)aStringList", value: ["hello", "world"]) - let storedValues = plugin.getAll() - XCTAssertEqual(storedValues["flutter.aBool"] as? Bool, true) - XCTAssertEqual(storedValues["flutter.aDouble"] as! Double, 3.14, accuracy: 0.0001) - XCTAssertEqual(storedValues["flutter.anInt"] as? Int, 42) - XCTAssertEqual(storedValues["flutter.aString"] as? String, "hello world") - XCTAssertEqual(storedValues["flutter.aStringList"] as? Array, ["hello", "world"]) + let storedValues = plugin.getAllWithPrefix(prefix: aPrefix) + XCTAssertEqual(storedValues["\(aPrefix)aBool"] as? Bool, true) + XCTAssertEqual(storedValues["\(aPrefix)aDouble"] as! Double, 3.14, accuracy: 0.0001) + XCTAssertEqual(storedValues["\(aPrefix)anInt"] as? Int, 42) + XCTAssertEqual(storedValues["\(aPrefix)aString"] as? String, "hello world") + XCTAssertEqual(storedValues["\(aPrefix)aStringList"] as? Array, ["hello", "world"]) + } } func testRemove() throws { - let plugin = SharedPreferencesPlugin() - let testKey = "flutter.foo" - plugin.setValue(key: testKey, value: 42) + for aPrefix in prefixes { + let plugin = SharedPreferencesPlugin() + let testKey = "\(aPrefix)foo" + plugin.setValue(key: testKey, value: 42) - // Make sure there is something to remove, so the test can't pass due to a set failure. - let preRemovalValues = plugin.getAll() - XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) + // Make sure there is something to remove, so the test can't pass due to a set failure. + let preRemovalValues = plugin.getAllWithPrefix(prefix: aPrefix) + XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) - // Then verify that removing it works. - plugin.remove(key: testKey) + // Then verify that removing it works. + plugin.remove(key: testKey) - let finalValues = plugin.getAll() - XCTAssertNil(finalValues[testKey] as Any?) + let finalValues = plugin.getAllWithPrefix(prefix: aPrefix) + XCTAssertNil(finalValues[testKey] as Any?) + } } func testClear() throws { - let plugin = SharedPreferencesPlugin() - let testKey = "flutter.foo" - plugin.setValue(key: testKey, value: 42) + for aPrefix in prefixes { + let plugin = SharedPreferencesPlugin() + let testKey = "\(aPrefix)foo" + plugin.setValue(key: testKey, value: 42) - // Make sure there is something to clear, so the test can't pass due to a set failure. - let preRemovalValues = plugin.getAll() - XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) + // Make sure there is something to clear, so the test can't pass due to a set failure. + let preRemovalValues = plugin.getAllWithPrefix(prefix: aPrefix) + XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) - // Then verify that clearing works. - plugin.clear() + // Then verify that clearing works. + plugin.clearWithPrefix(prefix: aPrefix) - let finalValues = plugin.getAll() - XCTAssertNil(finalValues[testKey] as Any?) + let finalValues = plugin.getAllWithPrefix(prefix: aPrefix) + XCTAssertNil(finalValues[testKey] as Any?) + } } + } diff --git a/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart index b3c1973c2cd..7a8fb4a669d 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart @@ -10,22 +10,36 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesFoundation', () { - const Map kTestValues = { + const Map flutterTestValues = { 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], }; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], }; + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + late SharedPreferencesStorePlatform preferences; setUp(() async { @@ -33,74 +47,220 @@ void main() { }); tearDown(() { - preferences.clear(); + preferences.clearWithPrefix(''); }); - // Normally the app-facing package adds the prefix, but since this test - // bypasses the app-facing package it needs to be manually added. - String prefixedKey(String key) { - return 'flutter.$key'; - } - testWidgets('reading', (WidgetTester _) async { - final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], isNull); - expect(values[prefixedKey('bool')], isNull); - expect(values[prefixedKey('int')], isNull); - expect(values[prefixedKey('double')], isNull); - expect(values[prefixedKey('List')], isNull); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], isNull); + expect(values['Bool'], isNull); + expect(values['Int'], isNull); + expect(values['Double'], isNull); + expect(values['StringList'], isNull); }); - testWidgets('writing', (WidgetTester _) async { + testWidgets('getAllWithPrefix', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', prefixedKey('String'), kTestValues2['flutter.String']!), + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), preferences.setValue( - 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), preferences.setValue( - 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), + 'String', 'flutter.String', allTestValues['flutter.String']!), preferences.setValue( - 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), preferences.setValue( - 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], allTestValues['prefix.String']); + expect(values['prefix.Bool'], allTestValues['prefix.Bool']); + expect(values['prefix.Int'], allTestValues['prefix.Int']); + expect(values['prefix.Double'], allTestValues['prefix.Double']); + expect(values['prefix.StringList'], allTestValues['prefix.StringList']); + }); + + testWidgets('clearWithPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), + preferences.setValue( + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix('prefix.'); + Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], null); + expect(values['prefix.Bool'], null); + expect(values['prefix.Int'], null); + expect(values['prefix.Double'], null); + expect(values['prefix.StringList'], null); + values = await preferences.getAllWithPrefix('flutter.'); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('getAllWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], allTestValues['String']); + expect(values['Bool'], allTestValues['Bool']); + expect(values['Int'], allTestValues['Int']); + expect(values['Double'], allTestValues['Double']); + expect(values['StringList'], allTestValues['StringList']); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('clearWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix(''); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], null); + expect(values['Bool'], null); + expect(values['Int'], null); + expect(values['Double'], null); + expect(values['StringList'], null); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); + }); + + testWidgets('getAll', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) ]); final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[prefixedKey('List')], kTestValues2['flutter.List']); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); }); - testWidgets('removing', (WidgetTester _) async { - final String key = prefixedKey('testKey'); - await preferences.setValue('String', key, kTestValues['flutter.String']!); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); - await preferences.setValue('Int', key, kTestValues['flutter.int']!); - await preferences.setValue('Double', key, kTestValues['flutter.double']!); + testWidgets('remove', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setValue( + 'String', key, allTestValues['flutter.String']!); + await preferences.setValue('Bool', key, allTestValues['flutter.Bool']!); + await preferences.setValue('Int', key, allTestValues['flutter.Int']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']!); + 'Double', key, allTestValues['flutter.Double']!); + await preferences.setValue( + 'StringList', key, allTestValues['flutter.StringList']!); await preferences.remove(key); - final Map values = await preferences.getAll(); + final Map values = await preferences.getAllWithPrefix(''); expect(values[key], isNull); }); - testWidgets('clearing', (WidgetTester _) async { + testWidgets('clear', (WidgetTester _) async { + await preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!); await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']!); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']!); + 'Int', 'flutter.Int', allTestValues['flutter.Int']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']!); + 'Double', 'flutter.Double', allTestValues['flutter.Double']!); + await preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!); await preferences.clear(); final Map values = await preferences.getAll(); - expect(values['String'], null); - expect(values['bool'], null); - expect(values['int'], null); - expect(values['double'], null); - expect(values['List'], null); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); + }); + + testWidgets('simultaneous writes', (WidgetTester _) async { + final List> writes = >[]; + const int writeCount = 100; + for (int i = 1; i <= writeCount; i++) { + writes.add(preferences.setValue('Int', 'Int', i)); + } + final List result = await Future.wait(writes, eagerError: true); + // All writes should succeed. + expect(result.where((bool element) => !element), isEmpty); + // The last write should win. + final Map values = await preferences.getAllWithPrefix(''); + expect(values['Int'], writeCount); }); }); } diff --git a/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart b/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart index f7c6c21567d..a74c34de57d 100644 --- a/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart +++ b/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart @@ -1,9 +1,10 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v5.0.0), do not edit directly. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -108,11 +109,12 @@ class UserDefaultsApi { } } - Future> getAll() async { + Future> getAllWithPrefix(String arg_prefix) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UserDefaultsApi.getAll', codec, + 'dev.flutter.pigeon.UserDefaultsApi.getAllWithPrefix', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; + final List? replyList = + await channel.send([arg_prefix]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -134,11 +136,12 @@ class UserDefaultsApi { } } - Future clear() async { + Future clearWithPrefix(String arg_prefix) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UserDefaultsApi.clear', codec, + 'dev.flutter.pigeon.UserDefaultsApi.clearWithPrefix', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; + final List? replyList = + await channel.send([arg_prefix]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart b/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart index 46b0ec41ea8..f535ab96206 100644 --- a/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart +++ b/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart @@ -11,6 +11,9 @@ typedef _Setter = Future Function(String key, Object value); /// iOS and macOS implementation of shared_preferences. class SharedPreferencesFoundation extends SharedPreferencesStorePlatform { final UserDefaultsApi _api = UserDefaultsApi(); + + static const String _defautPrefix = 'flutter.'; + late final Map _setters = { 'Bool': (String key, Object value) { return _api.setBool(key, value as bool); @@ -37,13 +40,23 @@ class SharedPreferencesFoundation extends SharedPreferencesStorePlatform { @override Future clear() async { - await _api.clear(); + return clearWithPrefix(_defautPrefix); + } + + @override + Future clearWithPrefix(String prefix) async { + await _api.clearWithPrefix(prefix); return true; } @override Future> getAll() async { - final Map result = await _api.getAll(); + return getAllWithPrefix(_defautPrefix); + } + + @override + Future> getAllWithPrefix(String prefix) async { + final Map result = await _api.getAllWithPrefix(prefix); return result.cast(); } diff --git a/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart index 81848ed5327..e651546b08d 100644 --- a/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart +++ b/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart @@ -20,6 +20,6 @@ abstract class UserDefaultsApi { void setValue(String key, Object value); // TODO(stuartmorgan): Make these non-nullable once // https://github.com/flutter/flutter/issues/97848 is fixed. - Map getAll(); - void clear(); + Map getAllWithPrefix(String prefix); + void clearWithPrefix(String prefix); } diff --git a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml index 74d7cfcf917..b6b91cce981 100644 --- a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_foundation description: iOS and macOS implementation of the shared_preferences plugin. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.5 +version: 2.2.0 environment: sdk: ">=2.17.0 <4.0.0" @@ -24,9 +24,9 @@ flutter: dependencies: flutter: sdk: flutter - shared_preferences_platform_interface: ^2.0.0 + shared_preferences_platform_interface: ^2.2.0 dev_dependencies: flutter_test: sdk: flutter - pigeon: ^5.0.0 + pigeon: ^9.0.0 diff --git a/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart b/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart index 6c0635a3342..5859aad9b65 100644 --- a/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart +++ b/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart @@ -13,8 +13,11 @@ class _MockSharedPreferencesApi implements TestUserDefaultsApi { final Map items = {}; @override - Map getAll() { - return items; + Map getAllWithPrefix(String prefix) { + return { + for (final String key in items.keys) + if (key.startsWith(prefix)) key: items[key] + }; } @override @@ -38,8 +41,12 @@ class _MockSharedPreferencesApi implements TestUserDefaultsApi { } @override - void clear() { - items.clear(); + void clearWithPrefix(String prefix) { + items.keys.toList().forEach((String key) { + if (key.startsWith(prefix)) { + items.remove(key); + } + }); } } @@ -47,6 +54,36 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late _MockSharedPreferencesApi api; + const Map flutterTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + setUp(() { api = _MockSharedPreferencesApi(); TestUserDefaultsApi.setup(api); @@ -72,21 +109,39 @@ void main() { expect(api.items.containsKey('flutter.hi'), isFalse); }); + test('clearWithPrefix', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + + Map all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 5); + await plugin.clearWithPrefix('prefix.'); + all = await plugin.getAll(); + expect(all.length, 5); + all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 0); + }); + test('getAll', () async { final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); - api.items['flutter.aBool'] = true; - api.items['flutter.aDouble'] = 3.14; - api.items['flutter.anInt'] = 42; - api.items['flutter.aString'] = 'hello world'; - api.items['flutter.aStringList'] = ['hello', 'world']; + for (final String key in flutterTestValues.keys) { + api.items[key] = flutterTestValues[key]!; + } final Map all = await plugin.getAll(); expect(all.length, 5); - expect(all['flutter.aBool'], api.items['flutter.aBool']); - expect(all['flutter.aDouble'], - closeTo(api.items['flutter.aDouble']! as num, 0.0001)); - expect(all['flutter.anInt'], api.items['flutter.anInt']); - expect(all['flutter.aString'], api.items['flutter.aString']); - expect(all['flutter.aStringList'], api.items['flutter.aStringList']); + expect(all, flutterTestValues); + }); + + test('getAllWithPrefix', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + final Map all = await plugin.getAllWithPrefix('prefix.'); + expect(all.length, 5); + expect(all, prefixTestValues); }); test('setValue', () async { @@ -112,4 +167,27 @@ void main() { await plugin.setValue('Map', 'flutter.key', {}); }, throwsA(isA())); }); + + test('getAllWithNoPrefix', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + final Map all = await plugin.getAllWithPrefix(''); + expect(all.length, 15); + expect(all, allTestValues); + }); + + test('clearWithNoPrefix', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); + for (final String key in allTestValues.keys) { + api.items[key] = allTestValues[key]!; + } + + Map all = await plugin.getAllWithPrefix(''); + expect(all.length, 15); + await plugin.clearWithPrefix(''); + all = await plugin.getAllWithPrefix(''); + expect(all.length, 0); + }); } diff --git a/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart b/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart index 12f97bd2b79..bbc0d2a7e6e 100644 --- a/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart +++ b/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v5.0.0), do not edit directly. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -24,9 +24,9 @@ abstract class TestUserDefaultsApi { void setValue(String key, Object value); - Map getAll(); + Map getAllWithPrefix(String prefix); - void clear(); + void clearWithPrefix(String prefix); static void setup(TestUserDefaultsApi? api, {BinaryMessenger? binaryMessenger}) { @@ -117,28 +117,39 @@ abstract class TestUserDefaultsApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UserDefaultsApi.getAll', codec, + 'dev.flutter.pigeon.UserDefaultsApi.getAllWithPrefix', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - // ignore message - final Map output = api.getAll(); + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.getAllWithPrefix was null.'); + final List args = (message as List?)!; + final String? arg_prefix = (args[0] as String?); + assert(arg_prefix != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.getAllWithPrefix was null, expected non-null String.'); + final Map output = + api.getAllWithPrefix(arg_prefix!); return [output]; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UserDefaultsApi.clear', codec, + 'dev.flutter.pigeon.UserDefaultsApi.clearWithPrefix', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - // ignore message - api.clear(); + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.clearWithPrefix was null.'); + final List args = (message as List?)!; + final String? arg_prefix = (args[0] as String?); + assert(arg_prefix != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.clearWithPrefix was null, expected non-null String.'); + api.clearWithPrefix(arg_prefix!); return []; }); } diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index d3736fa46bc..4ba0cec9781 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Adds `getAllWithPrefix` and `clearWithPrefix` methods. + ## 2.1.5 * Clarifies explanation of endorsement in README. diff --git a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart index 664048ab98e..8939d0a03bd 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -38,34 +38,36 @@ void main() { testWidgets('reading', (WidgetTester _) async { final Map all = await preferences.getAll(); - expect(all['String'], isNull); - expect(all['bool'], isNull); - expect(all['int'], isNull); - expect(all['double'], isNull); - expect(all['List'], isNull); + expect(all['flutter.String'], isNull); + expect(all['flutter.bool'], isNull); + expect(all['flutter.int'], isNull); + expect(all['flutter.double'], isNull); + expect(all['flutter.List'], isNull); }); testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']!), - preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!), - preferences.setValue('Int', 'int', kTestValues2['flutter.int']!), + 'String', 'flutter.String', kTestValues2['flutter.String']!), preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']!), + 'Bool', 'flutter.bool', kTestValues2['flutter.bool']!), preferences.setValue( - 'StringList', 'List', kTestValues2['flutter.List']!) + 'Int', 'flutter.int', kTestValues2['flutter.int']!), + preferences.setValue( + 'Double', 'flutter.double', kTestValues2['flutter.double']!), + preferences.setValue( + 'StringList', 'flutter.List', kTestValues2['flutter.List']!) ]); final Map all = await preferences.getAll(); - expect(all['String'], kTestValues2['flutter.String']); - expect(all['bool'], kTestValues2['flutter.bool']); - expect(all['int'], kTestValues2['flutter.int']); - expect(all['double'], kTestValues2['flutter.double']); - expect(all['List'], kTestValues2['flutter.List']); + expect(all['flutter.String'], kTestValues2['flutter.String']); + expect(all['flutter.bool'], kTestValues2['flutter.bool']); + expect(all['flutter.int'], kTestValues2['flutter.int']); + expect(all['flutter.double'], kTestValues2['flutter.double']); + expect(all['flutter.List'], kTestValues2['flutter.List']); }); testWidgets('removing', (WidgetTester _) async { - const String key = 'testKey'; + const String key = 'flutter.testKey'; await Future.wait(>[ preferences.setValue('String', key, kTestValues['flutter.String']!), @@ -76,26 +78,28 @@ void main() { ]); await preferences.remove(key); final Map all = await preferences.getAll(); - expect(all['testKey'], isNull); + expect(all[key], isNull); }); testWidgets('clearing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', 'String', kTestValues['flutter.String']!), - preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!), - preferences.setValue('Int', 'int', kTestValues['flutter.int']!), + 'String', 'flutter.String', kTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.bool', kTestValues['flutter.bool']!), + preferences.setValue('Int', 'flutter.int', kTestValues['flutter.int']!), + preferences.setValue( + 'Double', 'flutter.double', kTestValues['flutter.double']!), preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']!), - preferences.setValue('StringList', 'List', kTestValues['flutter.List']!) + 'StringList', 'flutter.List', kTestValues['flutter.List']!) ]); await preferences.clear(); final Map all = await preferences.getAll(); - expect(all['String'], null); - expect(all['bool'], null); - expect(all['int'], null); - expect(all['double'], null); - expect(all['List'], null); + expect(all['flutter.String'], null); + expect(all['flutter.bool'], null); + expect(all['flutter.int'], null); + expect(all['flutter.double'], null); + expect(all['flutter.List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index 1cc1c41f871..738ccdc6cba 100644 --- a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart @@ -21,6 +21,8 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @Deprecated('Use `SharedPreferencesStorePlatform.instance` instead.') static SharedPreferencesLinux instance = SharedPreferencesLinux(); + static const String _defaultPrefix = 'flutter.'; + /// Registers the Linux implementation. static void registerWith() { SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); @@ -46,13 +48,8 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { return fs.file(path.join(directory, 'shared_preferences.json')); } - /// Gets the preferences from the stored file. Once read, the preferences are - /// maintained in memory. - Future> _readPreferences() async { - if (_cachedPreferences != null) { - return _cachedPreferences!; - } - + /// Gets the preferences from the stored file and saves them in cache. + Future> _reload() async { Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { @@ -68,6 +65,12 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { return preferences; } + /// Checks for cached preferences and returns them or loads preferences from + /// file and returns and caches them. + Future> _readPreferences() async { + return _cachedPreferences ?? await _reload(); + } + /// Writes the cached preferences to disk. Returns [true] if the operation /// succeeded. Future _writePreferences(Map preferences) async { @@ -91,14 +94,27 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @override Future clear() async { + return clearWithPrefix(_defaultPrefix); + } + + @override + Future clearWithPrefix(String prefix) async { final Map preferences = await _readPreferences(); - preferences.clear(); + preferences.removeWhere((String key, _) => key.startsWith(prefix)); return _writePreferences(preferences); } @override Future> getAll() async { - return _readPreferences(); + return getAllWithPrefix(_defaultPrefix); + } + + @override + Future> getAllWithPrefix(String prefix) async { + final Map withPrefix = + Map.from(await _readPreferences()); + withPrefix.removeWhere((String key, _) => !key.startsWith(prefix)); + return withPrefix; } @override diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 08fc024c9de..ff3f6d2b92b 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.5 +version: 2.2.0 environment: sdk: ">=2.17.0 <4.0.0" @@ -22,7 +22,7 @@ dependencies: path: ^1.8.0 path_provider_linux: ^2.0.0 path_provider_platform_interface: ^2.0.0 - shared_preferences_platform_interface: ^2.0.0 + shared_preferences_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 176d1d9f9ea..a4f28543142 100644 --- a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart @@ -1,6 +1,8 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; @@ -15,6 +17,36 @@ void main() { SharedPreferencesLinux.registerWith(); + const Map flutterTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + setUp(() { fs = MemoryFileSystem.test(); pathProvider = FakePathProviderLinux(); @@ -49,13 +81,21 @@ void main() { }); test('getAll', () async { - await writeTestFile('{"key1": "one", "key2": 2}'); + await writeTestFile(json.encode(allTestValues)); final SharedPreferencesLinux prefs = getPreferences(); final Map values = await prefs.getAll(); - expect(values, hasLength(2)); - expect(values['key1'], 'one'); - expect(values['key2'], 2); + expect(values, hasLength(5)); + expect(values, flutterTestValues); + }); + + test('getAllWithPrefix', () async { + await writeTestFile(json.encode(allTestValues)); + final SharedPreferencesLinux prefs = getPreferences(); + + final Map values = await prefs.getAllWithPrefix('prefix.'); + expect(values, hasLength(5)); + expect(values, prefixTestValues); }); test('remove', () async { @@ -78,12 +118,43 @@ void main() { }); test('clear', () async { - await writeTestFile('{"key1":"one","key2":2}'); + await writeTestFile(json.encode(flutterTestValues)); final SharedPreferencesLinux prefs = getPreferences(); + expect(await readTestFile(), json.encode(flutterTestValues)); await prefs.clear(); expect(await readTestFile(), '{}'); }); + + test('clearWithPrefix', () async { + await writeTestFile(json.encode(flutterTestValues)); + final SharedPreferencesLinux prefs = getPreferences(); + await prefs.clearWithPrefix('prefix.'); + final Map noValues = + await prefs.getAllWithPrefix('prefix.'); + expect(noValues, hasLength(0)); + + final Map values = await prefs.getAll(); + expect(values, hasLength(5)); + expect(values, flutterTestValues); + }); + + test('getAllWithNoPrefix', () async { + await writeTestFile(json.encode(allTestValues)); + final SharedPreferencesLinux prefs = getPreferences(); + + final Map values = await prefs.getAllWithPrefix(''); + expect(values, hasLength(15)); + expect(values, allTestValues); + }); + + test('clearWithNoPrefix', () async { + await writeTestFile(json.encode(flutterTestValues)); + final SharedPreferencesLinux prefs = getPreferences(); + await prefs.clearWithPrefix(''); + final Map noValues = await prefs.getAllWithPrefix(''); + expect(noValues, hasLength(0)); + }); } /// Fake implementation of PathProviderLinux that returns hard-coded paths, diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 1f1d2dc031e..b5efabce677 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Adds `getAllWithPrefix` and `clearWithPrefix` methods. + ## 2.0.6 * Clarifies explanation of endorsement in README. diff --git a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart index d3bfa49af8a..ccc01df61c4 100644 --- a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert' show json; import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; @@ -37,57 +36,257 @@ void main() { isA()); }); - testWidgets('getAll', (WidgetTester tester) async { - final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - expect(await store.getAll(), isEmpty); + const Map flutterTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; - html.window.localStorage['flutter.testKey'] = '"test value"'; - html.window.localStorage['unprefixed_key'] = 'not a flutter value'; - final Map allData = await store.getAll(); - expect(allData, hasLength(1)); - expect(allData['flutter.testKey'], 'test value'); + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + + late SharedPreferencesStorePlatform preferences; + + setUp(() async { + preferences = SharedPreferencesStorePlatform.instance; }); - testWidgets('remove', (WidgetTester tester) async { - final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - html.window.localStorage['flutter.testKey'] = '"test value"'; - expect(html.window.localStorage['flutter.testKey'], isNotNull); - expect(await store.remove('flutter.testKey'), isTrue); - expect(html.window.localStorage['flutter.testKey'], isNull); - expect( - () => store.remove('unprefixed'), - throwsA(isA()), - ); + tearDown(() { + preferences.clearWithPrefix(''); }); - testWidgets('setValue', (WidgetTester tester) async { - final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - for (final String key in kTestValues.keys) { - final dynamic value = kTestValues[key]; - expect(await store.setValue(key.split('.').last, key, value), true); - } - expect(html.window.localStorage.keys, hasLength(kTestValues.length)); - for (final String key in html.window.localStorage.keys) { - expect(html.window.localStorage[key], json.encode(kTestValues[key])); - } + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], isNull); + expect(values['Bool'], isNull); + expect(values['Int'], isNull); + expect(values['Double'], isNull); + expect(values['StringList'], isNull); + }); + + testWidgets('getAllWithPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), + preferences.setValue( + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], allTestValues['prefix.String']); + expect(values['prefix.Bool'], allTestValues['prefix.Bool']); + expect(values['prefix.Int'], allTestValues['prefix.Int']); + expect(values['prefix.Double'], allTestValues['prefix.Double']); + expect(values['prefix.StringList'], allTestValues['prefix.StringList']); + }); - // Check that generics are preserved. - expect((await store.getAll())['flutter.StringList'], isA>()); + testWidgets('clearWithPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'prefix.String', allTestValues['prefix.String']!), + preferences.setValue( + 'Bool', 'prefix.Bool', allTestValues['prefix.Bool']!), + preferences.setValue('Int', 'prefix.Int', allTestValues['prefix.Int']!), + preferences.setValue( + 'Double', 'prefix.Double', allTestValues['prefix.Double']!), + preferences.setValue('StringList', 'prefix.StringList', + allTestValues['prefix.StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix('prefix.'); + Map values = + await preferences.getAllWithPrefix('prefix.'); + expect(values['prefix.String'], null); + expect(values['prefix.Bool'], null); + expect(values['prefix.Int'], null); + expect(values['prefix.Double'], null); + expect(values['prefix.StringList'], null); + values = await preferences.getAllWithPrefix('flutter.'); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('getAllWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], allTestValues['String']); + expect(values['Bool'], allTestValues['Bool']); + expect(values['Int'], allTestValues['Int']); + expect(values['Double'], allTestValues['Double']); + expect(values['StringList'], allTestValues['StringList']); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); + }); + + testWidgets('clearWithNoPrefix', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', allTestValues['String']!), + preferences.setValue('Bool', 'Bool', allTestValues['Bool']!), + preferences.setValue('Int', 'Int', allTestValues['Int']!), + preferences.setValue('Double', 'Double', allTestValues['Double']!), + preferences.setValue( + 'StringList', 'StringList', allTestValues['StringList']!), + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + await preferences.clearWithPrefix(''); + final Map values = await preferences.getAllWithPrefix(''); + expect(values['String'], null); + expect(values['Bool'], null); + expect(values['Int'], null); + expect(values['Double'], null); + expect(values['StringList'], null); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); + }); - // Invalid key format. - expect( - () => store.setValue('String', 'unprefixed', 'hello'), - throwsA(isA()), - ); + testWidgets('getAll', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!), + preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!), + preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!), + preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!), + preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!) + ]); + final Map values = await preferences.getAll(); + expect(values['flutter.String'], allTestValues['flutter.String']); + expect(values['flutter.Bool'], allTestValues['flutter.Bool']); + expect(values['flutter.Int'], allTestValues['flutter.Int']); + expect(values['flutter.Double'], allTestValues['flutter.Double']); + expect(values['flutter.StringList'], allTestValues['flutter.StringList']); }); - testWidgets('clear', (WidgetTester tester) async { - final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - html.window.localStorage['flutter.testKey1'] = '"test value"'; - html.window.localStorage['flutter.testKey2'] = '42'; - html.window.localStorage['unprefixed_key'] = 'not a flutter value'; - expect(await store.clear(), isTrue); - expect(html.window.localStorage.keys.single, 'unprefixed_key'); + testWidgets('remove', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setValue( + 'String', key, allTestValues['flutter.String']!); + await preferences.setValue('Bool', key, allTestValues['flutter.Bool']!); + await preferences.setValue('Int', key, allTestValues['flutter.Int']!); + await preferences.setValue( + 'Double', key, allTestValues['flutter.Double']!); + await preferences.setValue( + 'StringList', key, allTestValues['flutter.StringList']!); + await preferences.remove(key); + final Map values = await preferences.getAllWithPrefix(''); + expect(values[key], isNull); + }); + + testWidgets('clear', (WidgetTester _) async { + await preferences.setValue( + 'String', 'flutter.String', allTestValues['flutter.String']!); + await preferences.setValue( + 'Bool', 'flutter.Bool', allTestValues['flutter.Bool']!); + await preferences.setValue( + 'Int', 'flutter.Int', allTestValues['flutter.Int']!); + await preferences.setValue( + 'Double', 'flutter.Double', allTestValues['flutter.Double']!); + await preferences.setValue('StringList', 'flutter.StringList', + allTestValues['flutter.StringList']!); + await preferences.clear(); + final Map values = await preferences.getAll(); + expect(values['flutter.String'], null); + expect(values['flutter.Bool'], null); + expect(values['flutter.Int'], null); + expect(values['flutter.Double'], null); + expect(values['flutter.StringList'], null); + }); + + testWidgets('simultaneous writes', (WidgetTester _) async { + final List> writes = >[]; + const int writeCount = 100; + for (int i = 1; i <= writeCount; i++) { + writes.add(preferences.setValue('Int', 'Int', i)); + } + final List result = await Future.wait(writes, eagerError: true); + // All writes should succeed. + expect(result.where((bool element) => !element), isEmpty); + // The last write should win. + final Map values = await preferences.getAllWithPrefix(''); + expect(values['Int'], writeCount); }); }); } diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart index d9d62346518..6ee692e1d91 100644 --- a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart +++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart @@ -18,19 +18,31 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { SharedPreferencesStorePlatform.instance = SharedPreferencesPlugin(); } + static const String _defaultPrefix = 'flutter.'; + @override Future clear() async { + return clearWithPrefix(_defaultPrefix); + } + + @override + Future clearWithPrefix(String prefix) async { // IMPORTANT: Do not use html.window.localStorage.clear() as that will // remove _all_ local data, not just the keys prefixed with - // "flutter." - _storedFlutterKeys.forEach(html.window.localStorage.remove); + // _prefix + _getStoredFlutterKeys(prefix).forEach(html.window.localStorage.remove); return true; } @override Future> getAll() async { + return getAllWithPrefix(_defaultPrefix); + } + + @override + Future> getAllWithPrefix(String prefix) async { final Map allData = {}; - for (final String key in _storedFlutterKeys) { + for (final String key in _getStoredFlutterKeys(prefix)) { allData[key] = _decodeValue(html.window.localStorage[key]!); } return allData; @@ -38,31 +50,19 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - _checkPrefix(key); html.window.localStorage.remove(key); return true; } @override Future setValue(String valueType, String key, Object? value) async { - _checkPrefix(key); html.window.localStorage[key] = _encodeValue(value); return true; } - void _checkPrefix(String key) { - if (!key.startsWith('flutter.')) { - throw FormatException( - 'Shared preferences keys must start with prefix "flutter.".', - key, - 0, - ); - } - } - - Iterable get _storedFlutterKeys { + Iterable _getStoredFlutterKeys(String prefix) { return html.window.localStorage.keys - .where((String key) => key.startsWith('flutter.')); + .where((String key) => key.startsWith(prefix)); } String _encodeValue(Object? value) { diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index 3598348de23..5c88356b3ef 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.6 +version: 2.1.0 environment: sdk: ">=2.17.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - shared_preferences_platform_interface: ^2.0.0 + shared_preferences_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index cbde97a2018..caeabe4e04c 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Adds `getAllWithPrefix` and `clearWithPrefix` methods. + ## 2.1.5 * Clarifies explanation of endorsement in README. diff --git a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart index 92a34fc2a25..00252008e85 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart @@ -41,25 +41,27 @@ void main() { final SharedPreferencesWindows preferences = SharedPreferencesWindows(); preferences.clear(); await preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']!); - await preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!); - await preferences.setValue('Int', 'int', kTestValues2['flutter.int']!); + 'String', 'flutter.String', kTestValues2['flutter.String']!); await preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']!); + 'Bool', 'flutter.bool', kTestValues2['flutter.bool']!); await preferences.setValue( - 'StringList', 'List', kTestValues2['flutter.List']!); + 'Int', 'flutter.int', kTestValues2['flutter.int']!); + await preferences.setValue( + 'Double', 'flutter.double', kTestValues2['flutter.double']!); + await preferences.setValue( + 'StringList', 'flutter.List', kTestValues2['flutter.List']!); final Map values = await preferences.getAll(); - expect(values['String'], kTestValues2['flutter.String']); - expect(values['bool'], kTestValues2['flutter.bool']); - expect(values['int'], kTestValues2['flutter.int']); - expect(values['double'], kTestValues2['flutter.double']); - expect(values['List'], kTestValues2['flutter.List']); + expect(values['flutter.String'], kTestValues2['flutter.String']); + expect(values['flutter.bool'], kTestValues2['flutter.bool']); + expect(values['flutter.int'], kTestValues2['flutter.int']); + expect(values['flutter.double'], kTestValues2['flutter.double']); + expect(values['flutter.List'], kTestValues2['flutter.List']); }); testWidgets('removing', (WidgetTester _) async { final SharedPreferencesWindows preferences = SharedPreferencesWindows(); preferences.clear(); - const String key = 'testKey'; + const String key = 'flutter.testKey'; await preferences.setValue('String', key, kTestValues['flutter.String']!); await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); await preferences.setValue('Int', key, kTestValues['flutter.int']!); @@ -75,20 +77,22 @@ void main() { final SharedPreferencesWindows preferences = SharedPreferencesWindows(); preferences.clear(); await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']!); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); + 'String', 'flutter.String', kTestValues['flutter.String']!); + await preferences.setValue( + 'Bool', 'flutter.bool', kTestValues['flutter.bool']!); + await preferences.setValue( + 'Int', 'flutter.int', kTestValues['flutter.int']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']!); + 'Double', 'flutter.double', kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']!); + 'StringList', 'flutter.List', kTestValues['flutter.List']!); await preferences.clear(); final Map values = await preferences.getAll(); - expect(values['String'], null); - expect(values['bool'], null); - expect(values['int'], null); - expect(values['double'], null); - expect(values['List'], null); + expect(values['flutter.String'], null); + expect(values['flutter.bool'], null); + expect(values['flutter.int'], null); + expect(values['flutter.double'], null); + expect(values['flutter.List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index 5cdb30c04e0..63437594cec 100644 --- a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -26,6 +26,8 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); } + static const String _defaultPrefix = 'flutter.'; + /// File system used to store to disk. Exposed for testing only. @visibleForTesting FileSystem fs = const LocalFileSystem(); @@ -55,10 +57,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { /// Gets the preferences from the stored file. Once read, the preferences are /// maintained in memory. - Future> _readPreferences() async { - if (_cachedPreferences != null) { - return _cachedPreferences!; - } + Future> _reload() async { Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { @@ -74,6 +73,10 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { return preferences; } + Future> _readPreferences() async { + return _cachedPreferences ?? await _reload(); + } + /// Writes the cached preferences to disk. Returns [true] if the operation /// succeeded. Future _writePreferences(Map preferences) async { @@ -97,14 +100,27 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { @override Future clear() async { + return clearWithPrefix(_defaultPrefix); + } + + @override + Future clearWithPrefix(String prefix) async { final Map preferences = await _readPreferences(); - preferences.clear(); + preferences.removeWhere((String key, _) => key.startsWith(prefix)); return _writePreferences(preferences); } @override Future> getAll() async { - return _readPreferences(); + return getAllWithPrefix(_defaultPrefix); + } + + @override + Future> getAllWithPrefix(String prefix) async { + final Map withPrefix = + Map.from(await _readPreferences()); + withPrefix.removeWhere((String key, _) => !key.startsWith(prefix)); + return withPrefix; } @override diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index da7db58e3c5..ae2246b506c 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.5 +version: 2.2.0 environment: sdk: ">=2.17.0 <4.0.0" @@ -22,7 +22,7 @@ dependencies: path: ^1.8.0 path_provider_platform_interface: ^2.0.0 path_provider_windows: ^2.0.0 - shared_preferences_platform_interface: ^2.0.0 + shared_preferences_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index 04fa335b703..c23ac46e830 100644 --- a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; @@ -11,11 +13,43 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { - late MemoryFileSystem fileSystem; + late MemoryFileSystem fs; late PathProviderWindows pathProvider; + SharedPreferencesWindows.registerWith(); + + const Map flutterTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + + const Map prefixTestValues = { + 'prefix.String': 'hello world', + 'prefix.Bool': true, + 'prefix.Int': 42, + 'prefix.Double': 3.14159, + 'prefix.StringList': ['foo', 'bar'], + }; + + const Map nonPrefixTestValues = { + 'String': 'hello world', + 'Bool': true, + 'Int': 42, + 'Double': 3.14159, + 'StringList': ['foo', 'bar'], + }; + + final Map allTestValues = {}; + + allTestValues.addAll(flutterTestValues); + allTestValues.addAll(prefixTestValues); + allTestValues.addAll(nonPrefixTestValues); + setUp(() { - fileSystem = MemoryFileSystem.test(); + fs = MemoryFileSystem.test(); pathProvider = FakePathProviderWindows(); }); @@ -25,18 +59,18 @@ void main() { } Future writeTestFile(String value) async { - fileSystem.file(await getFilePath()) + fs.file(await getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); } Future readTestFile() async { - return fileSystem.file(await getFilePath()).readAsStringSync(); + return fs.file(await getFilePath()).readAsStringSync(); } SharedPreferencesWindows getPreferences() { final SharedPreferencesWindows prefs = SharedPreferencesWindows(); - prefs.fs = fileSystem; + prefs.fs = fs; prefs.pathProvider = pathProvider; return prefs; } @@ -48,13 +82,21 @@ void main() { }); test('getAll', () async { - await writeTestFile('{"key1": "one", "key2": 2}'); + await writeTestFile(json.encode(allTestValues)); final SharedPreferencesWindows prefs = getPreferences(); final Map values = await prefs.getAll(); - expect(values, hasLength(2)); - expect(values['key1'], 'one'); - expect(values['key2'], 2); + expect(values, hasLength(5)); + expect(values, flutterTestValues); + }); + + test('getAllWithPrefix', () async { + await writeTestFile(json.encode(allTestValues)); + final SharedPreferencesWindows prefs = getPreferences(); + + final Map values = await prefs.getAllWithPrefix('prefix.'); + expect(values, hasLength(5)); + expect(values, prefixTestValues); }); test('remove', () async { @@ -77,12 +119,43 @@ void main() { }); test('clear', () async { - await writeTestFile('{"key1":"one","key2":2}'); + await writeTestFile(json.encode(flutterTestValues)); final SharedPreferencesWindows prefs = getPreferences(); + expect(await readTestFile(), json.encode(flutterTestValues)); await prefs.clear(); expect(await readTestFile(), '{}'); }); + + test('clearWithPrefix', () async { + await writeTestFile(json.encode(flutterTestValues)); + final SharedPreferencesWindows prefs = getPreferences(); + await prefs.clearWithPrefix('prefix.'); + final Map noValues = + await prefs.getAllWithPrefix('prefix.'); + expect(noValues, hasLength(0)); + + final Map values = await prefs.getAll(); + expect(values, hasLength(5)); + expect(values, flutterTestValues); + }); + + test('getAllWithNoPrefix', () async { + await writeTestFile(json.encode(allTestValues)); + final SharedPreferencesWindows prefs = getPreferences(); + + final Map values = await prefs.getAllWithPrefix(''); + expect(values, hasLength(15)); + expect(values, allTestValues); + }); + + test('clearWithNoPrefix', () async { + await writeTestFile(json.encode(flutterTestValues)); + final SharedPreferencesWindows prefs = getPreferences(); + await prefs.clearWithPrefix(''); + final Map noValues = await prefs.getAllWithPrefix(''); + expect(noValues, hasLength(0)); + }); } /// Fake implementation of PathProviderWindows that returns hard-coded paths,