From 644000bfa34750b581582abf69c94facc11236dc Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Tue, 30 Jul 2024 13:30:20 -0700 Subject: [PATCH] [shared_preferences] full api redesign with DataStore and cache-less interface (#5210) Full rework for shared_preferences, allows user to decide whether to use cache or be fully async and pull direct from the platform. fixes https://github.com/flutter/flutter/issues/123078 fixes https://github.com/flutter/flutter/issues/133098 fixes https://github.com/flutter/flutter/issues/65145 fixes https://github.com/flutter/flutter/issues/64739 fixes https://github.com/flutter/flutter/issues/151036 --- .../shared_preferences/CHANGELOG.md | 3 +- .../shared_preferences/README.md | 132 ++- .../shared_preferences_test.dart | 720 +++++++++++--- .../shared_preferences/example/lib/main.dart | 30 +- .../example/lib/readme_excerpts.dart | 41 + .../shared_preferences/example/pubspec.yaml | 1 + .../lib/shared_preferences.dart | 285 +----- .../lib/src/shared_preferences_async.dart | 404 ++++++++ .../lib/src/shared_preferences_legacy.dart | 286 ++++++ .../shared_preferences/pubspec.yaml | 18 +- .../test/shared_preferences_async_test.dart | 919 ++++++++++++++++++ .../test/shared_preferences_test.dart | 38 +- .../CHANGELOG.md | 4 + ..._preferences_async_platform_interface.dart | 10 + .../pubspec.yaml | 2 +- 15 files changed, 2432 insertions(+), 461 deletions(-) create mode 100644 packages/shared_preferences/shared_preferences/lib/src/shared_preferences_async.dart create mode 100644 packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart create mode 100755 packages/shared_preferences/shared_preferences/test/shared_preferences_async_test.dart diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 80572617fd64..5df6b1f51dd7 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.3.0 +* Adds `SharedPreferencesAsync` and `SharedPreferencesWithCache` APIs. * Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. ## 2.2.3 diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index 1da1215df9ea..3fe18448dd3b 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -17,9 +17,48 @@ Supported data types are `int`, `double`, `bool`, `String` and `List`. ## Usage -### Examples +## SharedPreferences vs SharedPreferencesAsync vs SharedPreferencesWithCache + +Starting with version 2.3.0 there are three available APIs that can be used in this package. +[SharedPreferences] is a legacy API that will be deprecated in the future. We highly encourage +any new users of the plugin to use the newer [SharedPreferencesAsync] or [SharedPreferencesWithCache] +APIs instead. + +Consider migrating existing code to one of the new APIs. See [below](#migrating-from-sharedpreferences-to-sharedpreferencesasyncwithcache) +for more information. + +### Cache and async or sync getters + +[SharedPreferences] and [SharedPreferencesWithCache] both use a local cache to store preferences. +This allows for synchronous get calls after the initial setup call fetches the preferences from the platform. +However, The cache can present issues as well: + +- If you are using `shared_preferences` from multiple isolates, since each + isolate has its own singleton and cache. +- If you are using `shared_preferences` in multiple engine instances (including + those created by plugins that create background contexts on mobile devices, + such as `firebase_messaging`). +- If you are modifying the underlying system preference store through something + other than the `shared_preferences` plugin, such as native code. + +This can be remedied by calling the `reload` method before using a getter as needed. +If most get calls need a reload, consider using [SharedPreferencesAsync] instead. + +[SharedPreferencesAsync] does not utilize a local cache which causes all calls to be asynchronous +calls to the host platforms storage solution. This can be less performant, but should always provide the +latest data stored on the native platform regardless of what process was used to store it. + +### Android platform storage + +The [SharedPreferences] API uses the native [Android Shared Preferences](https://developer.android.com/reference/android/content/SharedPreferences) tool to store data. + +The [SharedPreferencesAsync] and [SharedPreferencesWithCache] APIs use [DataStore Preferences](https://developer.android.com/topic/libraries/architecture/datastore) to store data. + +## Examples Here are small examples that show you how to use the API. +### SharedPreferences + #### Write data ```dart @@ -60,32 +99,67 @@ final List? items = prefs.getStringList('items'); await prefs.remove('counter'); ``` -### Multiple instances +### SharedPreferencesAsync + +```dart +final SharedPreferencesAsync asyncPrefs = SharedPreferencesAsync(); -In order to make preference lookup via the `get*` methods synchronous, -`shared_preferences` uses a cache on the Dart side, which is normally only -updated by the `set*` methods. Usually this is an implementation detail that -does not affect callers, but it can cause issues in a few cases: -- If you are using `shared_preferences` from multiple isolates, since each - isolate has its own `SharedPreferences` singleton and cache. -- If you are using `shared_preferences` in multiple engine instances (including - those created by plugins that create background contexts on mobile devices, - such as `firebase_messaging`). -- If you are modifying the underlying system preference store through something - other than the `shared_preferences` plugin, such as native code. +await asyncPrefs.setBool('repeat', true); +await asyncPrefs.setString('action', 'Start'); + +final bool? repeat = await asyncPrefs.getBool('repeat'); +final String? action = await asyncPrefs.getString('action'); + +await asyncPrefs.remove('repeat'); + +// Any time a filter option is included as a method parameter, strongly consider +// using it to avoid potentially unwanted side effects. +await asyncPrefs.clear(allowList: {'action', 'repeat'}); +``` + +### SharedPreferencesWithCache + +```dart +final SharedPreferencesWithCache prefsWithCache = + await SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions( + // When an allowlist is included, any keys that aren't included cannot be used. + allowList: {'repeat', 'action'}, + ), +); -If you need to read a preference value that may have been changed by anything -other than the `SharedPreferences` instance you are reading it from, you should -call `reload()` on the instance before reading from it to update its cache with -any external changes. +await prefsWithCache.setBool('repeat', true); +await prefsWithCache.setString('action', 'Start'); + +final bool? repeat = prefsWithCache.getBool('repeat'); +final String? action = prefsWithCache.getString('action'); + +await prefsWithCache.remove('repeat'); + +// Since the filter options are set at creation, they aren't needed during clear. +await prefsWithCache.clear(); +``` -If this is problematic for your use case, you can thumbs up -[this issue](https://github.com/flutter/flutter/issues/123078) to express -interest in APIs that provide direct (asynchronous) access to the underlying -preference store, and/or subscribe to it for updates. ### Migration and Prefixes +#### Migrating from SharedPreferences to SharedPreferencesAsync/WithCache + +Currently, migration from the older [SharedPreferences] API to the newer +[SharedPreferencesAsync] or [SharedPreferencesWithCache] will need to be done manually. + +A simple form of this could be fetching all preferences with [SharedPreferences] and adding +them back using [SharedPreferencesAsync], then storing a preference indicating that the +migration has been done so that future runs don't repeat the migration. + +If a migration is not performed before moving to [SharedPreferencesAsync] or [SharedPreferencesWithCache], +most (if not all) data will be lost. Android preferences are stored in a new system, and all platforms +are likely to have some form of enforced prefix (see below) that would not transfer automatically. + +A tool to make this process easier can be tracked here: https://github.com/flutter/flutter/issues/150732 + +#### Adding, Removing, or changing prefixes on SharedPreferences + By default, the `SharedPreferences` plugin will only read (and write) preferences that begin with the prefix `flutter.`. This is all handled internally by the plugin and does not require manually adding this prefix. @@ -123,11 +197,11 @@ SharedPreferences.setMockInitialValues(values); ### Storage location by platform -| Platform | Location | -| :--- | :--- | -| Android | SharedPreferences | -| iOS | NSUserDefaults | -| Linux | In the XDG_DATA_HOME directory | -| macOS | NSUserDefaults | -| Web | LocalStorage | -| Windows | In the roaming AppData directory | +| Platform | SharedPreferences | SharedPreferencesAsync/WithCache | +| :--- | :--- | :--- | +| Android | SharedPreferences | DataStore Preferences | +| iOS | NSUserDefaults | NSUserDefaults | +| Linux | In the XDG_DATA_HOME directory | In the XDG_DATA_HOME directory | +| macOS | NSUserDefaults | NSUserDefaults | +| Web | LocalStorage | LocalStorage | +| Windows | In the roaming AppData directory | In the roaming AppData directory | diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index 443ac6fc4a79..08afb46b9b81 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -19,147 +19,641 @@ void main() { const bool testBool2 = false; const int testInt2 = 1337; const double testDouble2 = 2.71828; - const List testList2 = ['baz', 'quox']; - - late SharedPreferences preferences; - - void runAllTests() { - testWidgets('reading', (WidgetTester _) async { - expect(preferences.get('String'), isNull); - expect(preferences.get('bool'), isNull); - expect(preferences.get('int'), isNull); - expect(preferences.get('double'), isNull); - expect(preferences.get('List'), isNull); - expect(preferences.getString('String'), isNull); - expect(preferences.getBool('bool'), isNull); - expect(preferences.getInt('int'), isNull); - expect(preferences.getDouble('double'), isNull); - expect(preferences.getStringList('List'), isNull); - }); + const List testList2 = ['baz', 'qux']; - testWidgets('writing', (WidgetTester _) async { - await Future.wait(>[ - preferences.setString('String', testString2), - preferences.setBool('bool', testBool2), - preferences.setInt('int', testInt2), - preferences.setDouble('double', testDouble2), - preferences.setStringList('List', testList2) - ]); - expect(preferences.getString('String'), testString2); - expect(preferences.getBool('bool'), testBool2); - expect(preferences.getInt('int'), testInt2); - expect(preferences.getDouble('double'), testDouble2); - expect(preferences.getStringList('List'), testList2); - }); + group('shared_preferences', () { + late SharedPreferences preferences; - testWidgets('removing', (WidgetTester _) async { - const String key = 'testKey'; - await preferences.setString(key, testString); - await preferences.setBool(key, testBool); - await preferences.setInt(key, testInt); - await preferences.setDouble(key, testDouble); - await preferences.setStringList(key, testList); - await preferences.remove(key); - expect(preferences.get('testKey'), isNull); - }); + void runAllTests() { + testWidgets('set and get String', (WidgetTester _) async { + expect(preferences.get('String'), isNull); + await preferences.setString('String', testString2); + expect(preferences.getString('String'), testString2); + }); - testWidgets('clearing', (WidgetTester _) async { - await preferences.setString('String', testString); - await preferences.setBool('bool', testBool); - await preferences.setInt('int', testInt); - await preferences.setDouble('double', testDouble); - await preferences.setStringList('List', testList); - await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); - }); + testWidgets('set and get Bool', (WidgetTester _) async { + expect(preferences.get('Bool'), isNull); + await preferences.setBool('Bool', testBool2); + expect(preferences.getBool('Bool'), testBool2); + }); - testWidgets('simultaneous writes', (WidgetTester _) async { - final List> writes = >[]; - const int writeCount = 100; - for (int i = 1; i <= writeCount; i++) { - writes.add(preferences.setInt('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. - expect(preferences.getInt('int'), writeCount); - }); - } + testWidgets('set and get Int', (WidgetTester _) async { + expect(preferences.get('Int'), isNull); + await preferences.setInt('Int', testInt2); + expect(preferences.getInt('Int'), testInt2); + }); + + testWidgets('set and get Double', (WidgetTester _) async { + expect(preferences.get('Double'), isNull); + await preferences.setDouble('Double', testDouble2); + expect(preferences.getDouble('Double'), testDouble2); + }); - group('SharedPreferences', () { - setUp(() async { - preferences = await SharedPreferences.getInstance(); + testWidgets('set and get StringList', (WidgetTester _) async { + expect(preferences.get('StringList'), isNull); + await preferences.setStringList('StringList', testList2); + expect(preferences.getStringList('StringList'), testList2); + }); + + testWidgets('removing', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setString(key, testString); + await preferences.remove(key); + expect(preferences.get('testKey'), isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setString('String', testString); + await preferences.setBool('bool', testBool); + await preferences.setInt('int', testInt); + await preferences.setDouble('double', testDouble); + await preferences.setStringList('List', testList); + await preferences.clear(); + expect(preferences.getString('String'), null); + expect(preferences.getBool('bool'), null); + expect(preferences.getInt('int'), null); + expect(preferences.getDouble('double'), null); + expect(preferences.getStringList('List'), null); + }); + + testWidgets('simultaneous writes', (WidgetTester _) async { + final List> writes = >[]; + const int writeCount = 100; + for (int i = 1; i <= writeCount; i++) { + writes.add(preferences.setInt('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. + expect(preferences.getInt('int'), writeCount); + }); + } + + group('SharedPreferences', () { + setUp(() async { + preferences = await SharedPreferences.getInstance(); + }); + + tearDown(() async { + await preferences.clear(); + SharedPreferences.resetStatic(); + }); + + runAllTests(); }); - tearDown(() async { - await preferences.clear(); - SharedPreferences.resetStatic(); + group('setPrefix', () { + setUp(() async { + SharedPreferences.resetStatic(); + SharedPreferences.setPrefix('prefix.'); + preferences = await SharedPreferences.getInstance(); + }); + + tearDown(() async { + await preferences.clear(); + SharedPreferences.resetStatic(); + }); + + runAllTests(); }); - runAllTests(); - }); + group('setNoPrefix', () { + setUp(() async { + SharedPreferences.resetStatic(); + SharedPreferences.setPrefix(''); + preferences = await SharedPreferences.getInstance(); + }); - group('setPrefix', () { - setUp(() async { - SharedPreferences.resetStatic(); - SharedPreferences.setPrefix('prefix.'); - preferences = await SharedPreferences.getInstance(); + tearDown(() async { + await preferences.clear(); + SharedPreferences.resetStatic(); + }); + + runAllTests(); }); - tearDown(() async { - await preferences.clear(); + testWidgets('allowList only gets allowed items', (WidgetTester _) async { + const String allowedString = 'stringKey'; + const String allowedBool = 'boolKey'; + const String notAllowedDouble = 'doubleKey'; + const String resultString = 'resultString'; + + const Set allowList = {allowedString, allowedBool}; + SharedPreferences.resetStatic(); - }); + SharedPreferences.setPrefix('', allowList: allowList); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + + await prefs.setString(allowedString, resultString); + await prefs.setBool(allowedBool, true); + await prefs.setDouble(notAllowedDouble, 3.14); + + await prefs.reload(); + + final String? testString = prefs.getString(allowedString); + expect(testString, resultString); + + final bool? testBool = prefs.getBool(allowedBool); + expect(testBool, true); - runAllTests(); + final double? testDouble = prefs.getDouble(notAllowedDouble); + expect(testDouble, null); + }); }); - group('setNoPrefix', () { - setUp(() async { - SharedPreferences.resetStatic(); - SharedPreferences.setPrefix(''); - preferences = await SharedPreferences.getInstance(); + group('shared_preferences_async', () { + const String stringKey = 'testString'; + const String boolKey = 'testBool'; + const String intKey = 'testInt'; + const String doubleKey = 'testDouble'; + const String listKey = 'testList'; + + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + + group('Async', () { + Future getPreferences() async { + final SharedPreferencesAsync preferences = SharedPreferencesAsync(); + await preferences.clear(); + return preferences; + } + + testWidgets('set and get String', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + + await preferences.setString(stringKey, testString); + expect(await preferences.getString(stringKey), testString); + }); + + testWidgets('set and get bool', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + + await preferences.setBool(boolKey, testBool); + expect(await preferences.getBool(boolKey), testBool); + }); + + testWidgets('set and get int', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + + await preferences.setInt(intKey, testInt); + expect(await preferences.getInt(intKey), testInt); + }); + + testWidgets('set and get double', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + + await preferences.setDouble(doubleKey, testDouble); + expect(await preferences.getDouble(doubleKey), testDouble); + }); + + testWidgets('set and get StringList', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + + await preferences.setStringList(listKey, testList); + expect(await preferences.getStringList(listKey), testList); + }); + + testWidgets('getAll', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Map gotAll = await preferences.getAll(); + + expect(gotAll.length, 5); + expect(gotAll[stringKey], testString); + expect(gotAll[boolKey], testBool); + expect(gotAll[intKey], testInt); + expect(gotAll[doubleKey], testDouble); + expect(gotAll[listKey], testList); + }); + + testWidgets('getAll with filter', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Map gotAll = + await preferences.getAll(allowList: {stringKey, boolKey}); + + expect(gotAll.length, 2); + expect(gotAll[stringKey], testString); + expect(gotAll[boolKey], testBool); + }); + + testWidgets('getKeys', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = await preferences.getKeys(); + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + testWidgets('getKeys with filter', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = + await preferences.getKeys(allowList: {stringKey, boolKey}); + + expect(keys.length, 2); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + }); + + testWidgets('containsKey', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + const String key = 'testKey'; + + expect(false, await preferences.containsKey(key)); + + await preferences.setString(key, 'test'); + expect(true, await preferences.containsKey(key)); + }); + + testWidgets('clear', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + await preferences.clear(); + expect(await preferences.getString(stringKey), null); + expect(await preferences.getBool(boolKey), null); + expect(await preferences.getInt(intKey), null); + expect(await preferences.getDouble(doubleKey), null); + expect(await preferences.getStringList(listKey), null); + }); + + testWidgets('clear with filter', (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + await preferences.clear(allowList: {stringKey, boolKey}); + expect(await preferences.getString(stringKey), null); + expect(await preferences.getBool(boolKey), null); + expect(await preferences.getInt(intKey), testInt); + expect(await preferences.getDouble(doubleKey), testDouble); + expect(await preferences.getStringList(listKey), testList); + }); + + testWidgets('throws TypeError when returned getBool type is incorrect', + (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await preferences.setString(stringKey, testString); + + expect(() async { + await preferences.getBool(stringKey); + }, throwsA(isA())); + }); + + testWidgets('throws TypeError when returned getString type is incorrect', + (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await preferences.setInt(stringKey, testInt); + + expect(() async { + await preferences.getString(stringKey); + }, throwsA(isA())); + }); + + testWidgets('throws TypeError when returned getInt type is incorrect', + (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await preferences.setString(stringKey, testString); + + expect(() async { + await preferences.getInt(stringKey); + }, throwsA(isA())); + }); + + testWidgets('throws TypeError when returned getDouble type is incorrect', + (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await preferences.setString(stringKey, testString); + + expect(() async { + await preferences.getDouble(stringKey); + }, throwsA(isA())); + }); + + testWidgets( + 'throws TypeError when returned getStringList type is incorrect', + (WidgetTester _) async { + final SharedPreferencesAsync preferences = await getPreferences(); + await preferences.setString(stringKey, testString); + + expect(() async { + await preferences.getStringList(stringKey); + }, throwsA(isA())); + }); }); - tearDown(() async { - await preferences.clear(); - SharedPreferences.resetStatic(); + group('withCache', () { + Future< + ( + SharedPreferencesWithCache, + Map, + )> getPreferences() async { + final Map cache = {}; + final SharedPreferencesWithCache preferences = + await SharedPreferencesWithCache.create( + cache: cache, + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); + await preferences.clear(); + return (preferences, cache); + } + + testWidgets('set and get String', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + }); + + testWidgets('set and get bool', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setBool(boolKey, testBool); + expect(preferences.getBool(boolKey), testBool); + }); + + testWidgets('set and get int', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setInt(intKey, testInt); + expect(preferences.getInt(intKey), testInt); + }); + + testWidgets('set and get double', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setDouble(doubleKey, testDouble); + expect(preferences.getDouble(doubleKey), testDouble); + }); + + testWidgets('set and get StringList', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setStringList(listKey, testList); + expect(preferences.getStringList(listKey), testList); + }); + + testWidgets('reloading', (WidgetTester _) async { + final ( + SharedPreferencesWithCache preferences, + Map cache + ) = await getPreferences(); + await preferences.clear(); + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + + cache.clear(); + expect(preferences.getString(stringKey), null); + + await preferences.reloadCache(); + expect(preferences.getString(stringKey), testString); + }); + + testWidgets('containsKey', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + const String key = 'testKey'; + + expect(false, preferences.containsKey(key)); + + await preferences.setString(key, 'test'); + expect(true, preferences.containsKey(key)); + }); + + testWidgets('getKeys', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = preferences.keys; + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + testWidgets('clear', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + await preferences.clear(); + expect(preferences.getString(stringKey), null); + expect(preferences.getBool(boolKey), null); + expect(preferences.getInt(intKey), null); + expect(preferences.getDouble(doubleKey), null); + expect(preferences.getStringList(listKey), null); + }); }); - runAllTests(); - }); + group('withCache with filter', () { + Future< + ( + SharedPreferencesWithCache, + Map, + )> getPreferences() async { + final Map cache = {}; + final SharedPreferencesWithCache preferences = + await SharedPreferencesWithCache.create( + cache: cache, + cacheOptions: const SharedPreferencesWithCacheOptions( + allowList: { + stringKey, + boolKey, + intKey, + doubleKey, + listKey, + }, + ), + ); + await preferences.clear(); + return (preferences, cache); + } + + testWidgets('throws ArgumentError if key is not included in filter', + (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + const String key = 'testKey'; + + expect(() async => preferences.setString(key, 'test'), + throwsArgumentError); + }); - testWidgets('allowList only gets allowed items', (WidgetTester _) async { - const String allowedString = 'stringKey'; - const String allowedBool = 'boolKey'; - const String notAllowedDouble = 'doubleKey'; - const String resultString = 'resultString'; + testWidgets('set and get String', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); - const Set allowList = {allowedString, allowedBool}; + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + }); - SharedPreferences.resetStatic(); - SharedPreferences.setPrefix('', allowList: allowList); + testWidgets('set and get bool', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); - final SharedPreferences prefs = await SharedPreferences.getInstance(); + await preferences.setBool(boolKey, testBool); + expect(preferences.getBool(boolKey), testBool); + }); - await prefs.setString(allowedString, resultString); - await prefs.setBool(allowedBool, true); - await prefs.setDouble(notAllowedDouble, 3.14); + testWidgets('set and get int', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); - await prefs.reload(); + await preferences.setInt(intKey, testInt); + expect(preferences.getInt(intKey), testInt); + }); - final String? testString = prefs.getString(allowedString); - expect(testString, resultString); + testWidgets('set and get double', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); - final bool? testBool = prefs.getBool(allowedBool); - expect(testBool, true); + await preferences.setDouble(doubleKey, testDouble); + expect(preferences.getDouble(doubleKey), testDouble); + }); - final double? testDouble = prefs.getDouble(notAllowedDouble); - expect(testDouble, null); + testWidgets('set and get StringList', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + await preferences.setStringList(listKey, testList); + expect(preferences.getStringList(listKey), testList); + }); + + testWidgets('reloading', (WidgetTester _) async { + final ( + SharedPreferencesWithCache preferences, + Map cache + ) = await getPreferences(); + await preferences.clear(); + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + + cache.clear(); + expect(preferences.getString(stringKey), null); + + await preferences.reloadCache(); + expect(preferences.getString(stringKey), testString); + }); + + testWidgets('containsKey', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + + expect(false, preferences.containsKey(stringKey)); + + await preferences.setString(stringKey, 'test'); + expect(true, preferences.containsKey(stringKey)); + }); + + testWidgets('getKeys', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = preferences.keys; + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + testWidgets('clear', (WidgetTester _) async { + final (SharedPreferencesWithCache preferences, _) = + await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + await preferences.clear(); + + expect(preferences.getString(stringKey), null); + expect(preferences.getBool(boolKey), null); + // The data for the next few tests is still stored on the platform, but not in the cache. + // This will cause the results to be null. + expect(preferences.getInt(intKey), null); + expect(preferences.getDouble(doubleKey), null); + expect(preferences.getStringList(listKey), null); + }); + }); }); } diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 8f9dcd6d113a..003d8d911b53 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -19,7 +19,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( - title: 'SharedPreferences Demo', + title: 'SharedPreferencesWithCache Demo', home: SharedPreferencesDemo(), ); } @@ -33,33 +33,49 @@ class SharedPreferencesDemo extends StatefulWidget { } class SharedPreferencesDemoState extends State { - final Future _prefs = SharedPreferences.getInstance(); + final Future _prefs = + SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions( + // This cache will only accept the key 'counter'. + allowList: {'counter'})); late Future _counter; + int _externalCounter = 0; Future _incrementCounter() async { - final SharedPreferences prefs = await _prefs; + final SharedPreferencesWithCache prefs = await _prefs; final int counter = (prefs.getInt('counter') ?? 0) + 1; setState(() { - _counter = prefs.setInt('counter', counter).then((bool success) { + _counter = prefs.setInt('counter', counter).then((_) { return counter; }); }); } + /// Gets external button presses that could occur in another instance, thread, + /// or via some native system. + Future _getExternalCounter() async { + final SharedPreferencesAsync prefs = SharedPreferencesAsync(); + setState(() async { + _externalCounter = (await prefs.getInt('externalCounter')) ?? 0; + }); + } + @override void initState() { super.initState(); - _counter = _prefs.then((SharedPreferences prefs) { + _counter = _prefs.then((SharedPreferencesWithCache prefs) { return prefs.getInt('counter') ?? 0; }); + + _getExternalCounter(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('SharedPreferences Demo'), + title: const Text('SharedPreferencesWithCache Demo'), ), body: Center( child: FutureBuilder( @@ -75,7 +91,7 @@ class SharedPreferencesDemoState extends State { return Text('Error: ${snapshot.error}'); } else { return Text( - 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' + 'Button tapped ${snapshot.data ?? 0 + _externalCounter} time${(snapshot.data ?? 0 + _externalCounter) == 1 ? '' : 's'}.\n\n' 'This should persist across restarts.', ); } diff --git a/packages/shared_preferences/shared_preferences/example/lib/readme_excerpts.dart b/packages/shared_preferences/shared_preferences/example/lib/readme_excerpts.dart index 058b6736cb35..032409d49014 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/readme_excerpts.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/readme_excerpts.dart @@ -41,6 +41,47 @@ Future readmeSnippets() async { // #enddocregion Clear } +Future readmeSnippetsAsync() async { + // #docregion Async + final SharedPreferencesAsync asyncPrefs = SharedPreferencesAsync(); + + await asyncPrefs.setBool('repeat', true); + await asyncPrefs.setString('action', 'Start'); + + final bool? repeat = await asyncPrefs.getBool('repeat'); + final String? action = await asyncPrefs.getString('action'); + + await asyncPrefs.remove('repeat'); + + // Any time a filter option is included as a method parameter, strongly consider + // using it to avoid potentially unwanted side effects. + await asyncPrefs.clear(allowList: {'action', 'repeat'}); + // #enddocregion Async +} + +Future readmeSnippetsWithCache() async { + // #docregion WithCache + final SharedPreferencesWithCache prefsWithCache = + await SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions( + // When an allowlist is included, any keys that aren't included cannot be used. + allowList: {'repeat', 'action'}, + ), + ); + + await prefsWithCache.setBool('repeat', true); + await prefsWithCache.setString('action', 'Start'); + + final bool? repeat = prefsWithCache.getBool('repeat'); + final String? action = prefsWithCache.getString('action'); + + await prefsWithCache.remove('repeat'); + + // Since the filter options are set at creation, they aren't needed during clear. + await prefsWithCache.clear(); + // #enddocregion WithCache +} + // Uses test-only code. invalid_use_of_visible_for_testing_member is suppressed // for the whole file since otherwise there's no way to avoid it showing up in // the excerpt, and that is definitely not something people should be copying diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index d80cc7eacd50..a49f7f3b2b1a 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + shared_preferences_platform_interface: ^2.4.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 7361758b99d3..7dd15ff2c52a 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -2,286 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; -import 'package:shared_preferences_platform_interface/types.dart'; - -/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing -/// a persistent store for simple data. -/// -/// Data is persisted to disk asynchronously. -class SharedPreferences { - SharedPreferences._(this._preferenceCache); - - static String _prefix = 'flutter.'; - - static bool _prefixHasBeenChanged = false; - - static Set? _allowList; - - static Completer? _completer; - - static SharedPreferencesStorePlatform get _store => - SharedPreferencesStorePlatform.instance; - - /// Sets the prefix that is attached to all keys for all shared preferences. - /// - /// This changes the inputs when adding data to preferences as well as - /// setting the filter that determines what data will be returned - /// from the `getInstance` method. - /// - /// By default, the prefix is 'flutter.', which is compatible with the - /// previous behavior of this plugin. To use preferences with no prefix, - /// set [prefix] to ''. - /// - /// If [prefix] is set to '', you may encounter preferences that are - /// incompatible with shared_preferences. The optional parameter - /// [allowList] will cause the plugin to only return preferences that - /// are both contained in the list AND match the provided prefix. - /// - /// No migration of existing preferences is performed by this method. - /// If you set a different prefix, and have previously stored preferences, - /// you will need to handle any migration yourself. - /// - /// This cannot be called after `getInstance`. - static void setPrefix(String prefix, {Set? allowList}) { - if (_completer != null) { - throw StateError('setPrefix cannot be called after getInstance'); - } - _prefix = prefix; - _prefixHasBeenChanged = true; - _allowList = allowList; - } - - /// Resets class's static values to allow for testing of setPrefix flow. - @visibleForTesting - static void resetStatic() { - _completer = null; - _prefix = 'flutter.'; - _prefixHasBeenChanged = false; - _allowList = null; - } - - /// Loads and parses the [SharedPreferences] for this app from disk. - /// - /// Because this is reading from disk, it shouldn't be awaited in - /// performance-sensitive blocks. - static Future getInstance() async { - if (_completer == null) { - final Completer completer = - Completer(); - _completer = completer; - try { - final Map preferencesMap = - await _getSharedPreferencesMap(); - completer.complete(SharedPreferences._(preferencesMap)); - } catch (e) { - // If there's an error, explicitly return the future with an error. - // then set the completer to null so we can retry. - completer.completeError(e); - final Future sharedPrefsFuture = completer.future; - _completer = null; - return sharedPrefsFuture; - } - } - return _completer!.future; - } - - /// The cache that holds all preferences. - /// - /// It is instantiated to the current state of the SharedPreferences or - /// NSUserDefaults object and then kept in sync via setter methods in this - /// class. - /// - /// It is NOT guaranteed that this cache and the device prefs will remain - /// in sync since the setter method might fail for any reason. - final Map _preferenceCache; - - /// Returns all keys in the persistent storage. - Set getKeys() => Set.from(_preferenceCache.keys); - - /// Reads a value of any type from persistent storage. - Object? get(String key) => _preferenceCache[key]; - - /// Reads a value from persistent storage, throwing an exception if it's not a - /// bool. - bool? getBool(String key) => _preferenceCache[key] as bool?; - - /// Reads a value from persistent storage, throwing an exception if it's not - /// an int. - int? getInt(String key) => _preferenceCache[key] as int?; - - /// Reads a value from persistent storage, throwing an exception if it's not a - /// double. - double? getDouble(String key) => _preferenceCache[key] as double?; - - /// Reads a value from persistent storage, throwing an exception if it's not a - /// String. - String? getString(String key) => _preferenceCache[key] as String?; - - /// Returns true if the persistent storage contains the given [key]. - bool containsKey(String key) => _preferenceCache.containsKey(key); - - /// Reads a set of string values from persistent storage, throwing an - /// exception if it's not a string set. - List? getStringList(String key) { - List? list = _preferenceCache[key] as List?; - if (list != null && list is! List) { - list = list.cast().toList(); - _preferenceCache[key] = list; - } - // Make a copy of the list so that later mutations won't propagate - return list?.toList() as List?; - } - - /// Saves a boolean [value] to persistent storage in the background. - Future setBool(String key, bool value) => _setValue('Bool', key, value); - - /// Saves an integer [value] to persistent storage in the background. - Future setInt(String key, int value) => _setValue('Int', key, value); - - /// Saves a double [value] to persistent storage in the background. - /// - /// Android doesn't support storing doubles, so it will be stored as a float. - Future setDouble(String key, double value) => - _setValue('Double', key, value); - - /// Saves a string [value] to persistent storage in the background. - /// - /// Note: Due to limitations in Android's SharedPreferences, - /// values cannot start with any one of the following: - /// - /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' - /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' - /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' - Future setString(String key, String value) => - _setValue('String', key, value); - - /// Saves a list of strings [value] to persistent storage in the background. - Future setStringList(String key, List value) => - _setValue('StringList', key, value); - - /// Removes an entry from persistent storage. - Future remove(String key) { - final String prefixedKey = '$_prefix$key'; - _preferenceCache.remove(key); - return _store.remove(prefixedKey); - } - - Future _setValue(String valueType, String key, Object value) { - ArgumentError.checkNotNull(value, 'value'); - final String prefixedKey = '$_prefix$key'; - if (value is List) { - // Make a copy of the list so that later mutations won't propagate - _preferenceCache[key] = value.toList(); - } else { - _preferenceCache[key] = value; - } - return _store.setValue(valueType, prefixedKey, value); - } - - /// Always returns true. - /// On iOS, synchronize is marked deprecated. On Android, we commit every set. - @Deprecated('This method is now a no-op, and should no longer be called.') - Future commit() async => true; - - /// Completes with true once the user preferences for the app has been cleared. - Future clear() { - _preferenceCache.clear(); - if (_prefixHasBeenChanged) { - try { - return _store.clearWithParameters( - ClearParameters( - filter: PreferencesFilter( - prefix: _prefix, - allowList: _allowList, - ), - ), - ); - } catch (e) { - // Catching and clarifying UnimplementedError to provide a more robust message. - if (e is UnimplementedError) { - throw UnimplementedError(''' -This implementation of Shared Preferences doesn't yet support the setPrefix method. -Either update the implementation to support setPrefix, or do not call setPrefix. - '''); - } else { - rethrow; - } - } - } - return _store.clear(); - } - - /// Fetches the latest values from the host platform. - /// - /// Use this method to observe modifications that were made in native code - /// (without using the plugin) while the app is running. - Future reload() async { - final Map preferences = - await SharedPreferences._getSharedPreferencesMap(); - _preferenceCache.clear(); - _preferenceCache.addAll(preferences); - } - - static Future> _getSharedPreferencesMap() async { - final Map fromSystem = {}; - if (_prefixHasBeenChanged) { - try { - fromSystem.addAll( - await _store.getAllWithParameters( - GetAllParameters( - filter: PreferencesFilter( - prefix: _prefix, - allowList: _allowList, - ), - ), - ), - ); - } catch (e) { - // Catching and clarifying UnimplementedError to provide a more robust message. - if (e is UnimplementedError) { - throw UnimplementedError(''' -This implementation of Shared Preferences doesn't yet support the setPrefix method. -Either update the implementation to support setPrefix, or do not call setPrefix. - '''); - } else { - rethrow; - } - } - } else { - fromSystem.addAll(await _store.getAll()); - } - - if (_prefix.isEmpty) { - return fromSystem; - } - // Strip the prefix from the returned preferences. - final Map preferencesMap = {}; - for (final String key in fromSystem.keys) { - assert(key.startsWith(_prefix)); - preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; - } - return preferencesMap; - } - - /// Initializes the shared preferences with mock values for testing. - /// - /// If the singleton instance has been initialized already, it is nullified. - @visibleForTesting - static void setMockInitialValues(Map values) { - final Map newValues = - values.map((String key, Object value) { - String newKey = key; - if (!key.startsWith(_prefix)) { - newKey = '$_prefix$key'; - } - return MapEntry(newKey, value); - }); - SharedPreferencesStorePlatform.instance = - InMemorySharedPreferencesStore.withData(newValues); - _completer = null; - } -} +export 'src/shared_preferences_async.dart'; +export 'src/shared_preferences_legacy.dart'; diff --git a/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_async.dart b/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_async.dart new file mode 100644 index 000000000000..56ea4e33dfab --- /dev/null +++ b/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_async.dart @@ -0,0 +1,404 @@ +// 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:async'; + +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +/// Provides a persistent store for simple data. +/// +/// Data is persisted to and fetched from the disk asynchronously. +/// If synchronous access to preferences in a locally cached version of preferences +/// is preferred, consider using [SharedPreferencesWithCache] instead. +@immutable +class SharedPreferencesAsync { + /// Creates a new instance with the given [options]. + SharedPreferencesAsync({ + SharedPreferencesOptions options = const SharedPreferencesOptions(), + }) : _options = options { + if (SharedPreferencesAsyncPlatform.instance == null) { + throw StateError( + 'The SharedPreferencesAsyncPlatform instance must be set.'); + } else { + _platform = SharedPreferencesAsyncPlatform.instance!; + } + } + + /// Options that determine the behavior of contained methods, usually + /// platform specific extensions of the [SharedPreferencesOptions] class. + final SharedPreferencesOptions _options; + + late final SharedPreferencesAsyncPlatform _platform; + + /// Returns all keys on the the platform that match provided [parameters]. + /// + /// If no restrictions are provided, fetches all keys stored on the platform. + /// + /// Ignores any keys whose values are types which are incompatible with shared_preferences. + Future> getKeys({Set? allowList}) async { + final GetPreferencesParameters parameters = GetPreferencesParameters( + filter: PreferencesFilters(allowList: allowList)); + return _platform.getKeys(parameters, _options); + } + + /// Returns all keys and values on the the platform that match provided [parameters]. + /// + /// If no restrictions are provided, fetches all entries stored on the platform. + /// + /// Ignores any entries of types which are incompatible with shared_preferences. + Future> getAll({Set? allowList}) async { + final GetPreferencesParameters parameters = GetPreferencesParameters( + filter: PreferencesFilters(allowList: allowList)); + return _platform.getPreferences(parameters, _options); + } + + /// Reads a value from the platform, throwing a [TypeError] if the value is + /// not a bool. + Future getBool(String key) async { + return _platform.getBool(key, _options); + } + + /// Reads a value from the platform, throwing a [TypeError] if the value is + /// not an int. + Future getInt(String key) async { + return _platform.getInt(key, _options); + } + + /// Reads a value from the platform, throwing a [TypeError] if the value is + /// not a double. + Future getDouble(String key) async { + return _platform.getDouble(key, _options); + } + + /// Reads a value from the platform, throwing a [TypeError] if the value is + /// not a String. + Future getString(String key) async { + return _platform.getString(key, _options); + } + + /// Reads a list of string values from the platform, throwing a [TypeError] + /// if the value not a List. + Future?> getStringList(String key) async { + return _platform.getStringList(key, _options); + } + + /// Returns true if the the platform contains the given [key]. + Future containsKey(String key) async { + return (await getKeys(allowList: {key})).isNotEmpty; + } + + /// Saves a boolean [value] to the platform. + Future setBool(String key, bool value) { + return _platform.setBool(key, value, _options); + } + + /// Saves an integer [value] to the platform. + Future setInt(String key, int value) { + return _platform.setInt(key, value, _options); + } + + /// Saves a double [value] to the platform. + /// + /// On platforms that do not support storing doubles, + /// the value will be stored as a float. + Future setDouble(String key, double value) { + return _platform.setDouble(key, value, _options); + } + + /// Saves a string [value] to the platform. + /// + /// Some platforms have special values that cannot be stored, please refer to + /// the README for more information. + Future setString(String key, String value) { + return _platform.setString(key, value, _options); + } + + /// Saves a list of strings [value] to the platform. + Future setStringList(String key, List value) { + return _platform.setStringList(key, value, _options); + } + + /// Removes an entry from the platform. + Future remove(String key) { + return _platform.clear( + ClearPreferencesParameters( + filter: PreferencesFilters(allowList: {key})), + _options); + } + + /// Clears all preferences from the platform. + /// + /// If no [parameters] are provided, and [SharedPreferencesAsync] has no filter, + /// all preferences will be removed. This may include values not set by this instance, + /// such as those stored by native code or by other packages using + /// shared_preferences internally, which may cause unintended side effects. + /// + /// It is highly recommended that an [allowList] be provided to this call. + Future clear({Set? allowList}) { + final ClearPreferencesParameters parameters = ClearPreferencesParameters( + filter: PreferencesFilters(allowList: allowList)); + return _platform.clear(parameters, _options); + } +} + +/// Options necessary to create a [SharedPreferencesWithCache]. +class SharedPreferencesWithCacheOptions { + /// Creates a new instance with the given options. + const SharedPreferencesWithCacheOptions({ + this.allowList, + }); + + /// Information about what data will be fetched during `get` and `init` + /// methods, what data can be `set`, as well as what data will be removed by `clear`. + /// + /// A `null` allowList will prevent filtering, allowing all items to be cached. + /// An empty allowList will prevent all caching as well as getting and setting. + /// + /// Setting an allowList is strongly recommended, to prevent getting and + /// caching unneeded or unexpected data. + final Set? allowList; +} + +/// Provides a persistent store for simple data. +/// +/// Cache provided to allow for synchronous gets. +/// +/// If preferences on the platform may be altered by other means than through +/// this instance, consider using [SharedPreferencesAsync] instead. You may also +/// refresh the cached data using [reloadCache] prior to a get request to prevent +/// missed changes that may have occurred since the cache was last updated. +@immutable +class SharedPreferencesWithCache { + /// Creates a new instance with the given options. + SharedPreferencesWithCache._create({ + required SharedPreferencesOptions sharedPreferencesOptions, + required SharedPreferencesWithCacheOptions cacheOptions, + Map? cache, + }) : _cacheOptions = cacheOptions, + _platformMethods = + SharedPreferencesAsync(options: sharedPreferencesOptions), + _cache = cache ?? {}; + + /// Creates a new instance with the given options and reloads the cache from + /// the platform data. + static Future create({ + SharedPreferencesOptions sharedPreferencesOptions = + const SharedPreferencesOptions(), + required SharedPreferencesWithCacheOptions cacheOptions, + Map? cache, + }) async { + final SharedPreferencesWithCache preferences = + SharedPreferencesWithCache._create( + sharedPreferencesOptions: sharedPreferencesOptions, + cacheOptions: cacheOptions, + cache: cache, + ); + + await preferences.reloadCache(); + + return preferences; + } + + /// Cache containing in-memory data. + final Map _cache; + + /// Options that define cache behavior. + final SharedPreferencesWithCacheOptions _cacheOptions; + + /// Async access directly to the platform. + /// + /// Methods called through [_platformMethods] will NOT update the cache. + final SharedPreferencesAsync _platformMethods; + + /// Updates cache with latest values from platform. + /// + /// This should be called before reading any values if the values may have + /// been changed by anything other than this cache instance, + /// such as from another isolate or native code that changes the underlying + /// preference storage directly. + Future reloadCache() async { + _cache.clear(); + _cache.addAll( + await _platformMethods.getAll(allowList: _cacheOptions.allowList)); + } + + /// Returns true if cache contains the given [key]. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + bool containsKey(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return _cache.containsKey(key); + } + + /// Returns all keys in the cache. + Set get keys => _cache.keys.toSet(); + + /// Reads a value of any type from the cache. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Object? get(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return _cache[key]; + } + + /// Reads a value from the cache, throwing a [TypeError] if the value is not a + /// bool. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + bool? getBool(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return get(key) as bool?; + } + + /// Reads a value from the cache, throwing a [TypeError] if the value is not + /// an int. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + int? getInt(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return get(key) as int?; + } + + /// Reads a value from the cache, throwing a [TypeError] if the value is not a + /// double. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + double? getDouble(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return get(key) as double?; + } + + /// Reads a value from the cache, throwing a [TypeError] if the value is not a + /// String. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + String? getString(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + return get(key) as String?; + } + + /// Reads a list of string values from the cache, throwing an + /// exception if it's not a string list. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + List? getStringList(String key) { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + final List? list = _cache[key] as List?; + // Make a copy of the list so that later mutations won't propagate + return list?.toList(); + } + + /// Saves a boolean [value] to the cache and platform. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future setBool(String key, bool value) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache[key] = value; + return _platformMethods.setBool(key, value); + } + + /// Saves an integer [value] to the cache and platform. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future setInt(String key, int value) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache[key] = value; + return _platformMethods.setInt(key, value); + } + + /// Saves a double [value] to the cache and platform. + /// + /// On platforms that do not support storing doubles, + /// the value will be stored as a float instead. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future setDouble(String key, double value) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache[key] = value; + return _platformMethods.setDouble(key, value); + } + + /// Saves a string [value] to the cache and platform. + /// + /// Note: Due to limitations on some platforms, + /// values cannot start with the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future setString(String key, String value) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache[key] = value; + return _platformMethods.setString(key, value); + } + + /// Saves a list of strings [value] to the cache and platform. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future setStringList(String key, List value) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache[key] = value; + return _platformMethods.setStringList(key, value); + } + + /// Removes an entry from cache and platform. + /// + /// Throws an [ArgumentError] if [key] is not in this instance's filter. + Future remove(String key) async { + if (!_isValidKey(key)) { + throw ArgumentError( + '$key is not included in the PreferencesFilter allowlist'); + } + _cache.remove(key); + return _platformMethods.remove(key); + } + + /// Clears cache and platform preferences that match filter options. + Future clear() async { + _cache.clear(); + return _platformMethods.clear(allowList: _cacheOptions.allowList); + } + + bool _isValidKey(String key) { + return _cacheOptions.allowList?.contains(key) ?? true; + } +} diff --git a/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart b/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart new file mode 100644 index 000000000000..a7eccb169504 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart @@ -0,0 +1,286 @@ +// 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:async'; + +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing +/// a persistent store for simple data. +/// +/// Data is persisted to disk asynchronously. +/// +/// This is a legacy API. For new code, consider [SharedPreferencesAsync] or [SharedPreferencesWithCache]. +class SharedPreferences { + SharedPreferences._(this._preferenceCache); + + static String _prefix = 'flutter.'; + + static bool _prefixHasBeenChanged = false; + + static Set? _allowList; + + static Completer? _completer; + + static SharedPreferencesStorePlatform get _store => + SharedPreferencesStorePlatform.instance; + + /// Sets the prefix that is attached to all keys for all shared preferences. + /// + /// This changes the inputs when adding data to preferences as well as + /// setting the filter that determines what data will be returned + /// from the `getInstance` method. + /// + /// By default, the prefix is 'flutter.', which is compatible with the + /// previous behavior of this plugin. To use preferences with no prefix, + /// set [prefix] to ''. + /// + /// If [prefix] is set to '', you may encounter preferences that are + /// incompatible with shared_preferences. The optional parameter + /// [allowList] will cause the plugin to only return preferences that + /// are both contained in the list AND match the provided prefix. + /// + /// No migration of existing preferences is performed by this method. + /// If you set a different prefix, and have previously stored preferences, + /// you will need to handle any migration yourself. + /// + /// This cannot be called after `getInstance`. + static void setPrefix(String prefix, {Set? allowList}) { + if (_completer != null) { + throw StateError('setPrefix cannot be called after getInstance'); + } + _prefix = prefix; + _prefixHasBeenChanged = true; + _allowList = allowList; + } + + /// Resets class's static values to allow for testing of setPrefix flow. + @visibleForTesting + static void resetStatic() { + _completer = null; + _prefix = 'flutter.'; + _prefixHasBeenChanged = false; + _allowList = null; + } + + /// Loads and parses the [SharedPreferences] for this app from disk. + /// + /// Because this is reading from disk, it shouldn't be awaited in + /// performance-sensitive blocks. + static Future getInstance() async { + if (_completer == null) { + final Completer completer = + Completer(); + _completer = completer; + try { + final Map preferencesMap = + await _getSharedPreferencesMap(); + completer.complete(SharedPreferences._(preferencesMap)); + } catch (e) { + // If there's an error, explicitly return the future with an error. + // then set the completer to null so we can retry. + completer.completeError(e); + final Future sharedPrefsFuture = completer.future; + _completer = null; + return sharedPrefsFuture; + } + } + return _completer!.future; + } + + /// The cache that holds all preferences. + /// + /// It is instantiated to the current state of the SharedPreferences or + /// NSUserDefaults object and then kept in sync via setter methods in this + /// class. + /// + /// It is NOT guaranteed that this cache and the device prefs will remain + /// in sync since the setter method might fail for any reason. + final Map _preferenceCache; + + /// Returns all keys in the persistent storage. + Set getKeys() => Set.from(_preferenceCache.keys); + + /// Reads a value of any type from persistent storage. + Object? get(String key) => _preferenceCache[key]; + + /// Reads a value from persistent storage, throwing an exception if it's not a + /// bool. + bool? getBool(String key) => _preferenceCache[key] as bool?; + + /// Reads a value from persistent storage, throwing an exception if it's not + /// an int. + int? getInt(String key) => _preferenceCache[key] as int?; + + /// Reads a value from persistent storage, throwing an exception if it's not a + /// double. + double? getDouble(String key) => _preferenceCache[key] as double?; + + /// Reads a value from persistent storage, throwing an exception if it's not a + /// String. + String? getString(String key) => _preferenceCache[key] as String?; + + /// Returns true if the persistent storage contains the given [key]. + bool containsKey(String key) => _preferenceCache.containsKey(key); + + /// Reads a set of string values from persistent storage, throwing an + /// exception if it's not a string list. + List? getStringList(String key) { + List? list = _preferenceCache[key] as List?; + list = list?.cast(); + // Make a copy of the list so that later mutations won't propagate + return list?.toList() as List?; + } + + /// Saves a boolean [value] to persistent storage in the background. + Future setBool(String key, bool value) => _setValue('Bool', key, value); + + /// Saves an integer [value] to persistent storage in the background. + Future setInt(String key, int value) => _setValue('Int', key, value); + + /// Saves a double [value] to persistent storage in the background. + /// + /// Android doesn't support storing doubles, so it will be stored as a float. + Future setDouble(String key, double value) => + _setValue('Double', key, value); + + /// Saves a string [value] to persistent storage in the background. + /// + /// Note: Due to limitations in Android's SharedPreferences, + /// values cannot start with any one of the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + Future setString(String key, String value) => + _setValue('String', key, value); + + /// Saves a list of strings [value] to persistent storage in the background. + Future setStringList(String key, List value) => + _setValue('StringList', key, value); + + /// Removes an entry from persistent storage. + Future remove(String key) { + final String prefixedKey = '$_prefix$key'; + _preferenceCache.remove(key); + return _store.remove(prefixedKey); + } + + Future _setValue(String valueType, String key, Object value) { + ArgumentError.checkNotNull(value, 'value'); + final String prefixedKey = '$_prefix$key'; + if (value is List) { + // Make a copy of the list so that later mutations won't propagate + _preferenceCache[key] = value.toList(); + } else { + _preferenceCache[key] = value; + } + return _store.setValue(valueType, prefixedKey, value); + } + + /// Always returns true. + /// On iOS, synchronize is marked deprecated. On Android, we commit every set. + @Deprecated('This method is now a no-op, and should no longer be called.') + Future commit() async => true; + + /// Completes with true once the user preferences for the app has been cleared. + Future clear() { + _preferenceCache.clear(); + if (_prefixHasBeenChanged) { + try { + return _store.clearWithParameters( + ClearParameters( + filter: PreferencesFilter( + prefix: _prefix, + allowList: _allowList, + ), + ), + ); + } catch (e) { + // Catching and clarifying UnimplementedError to provide a more robust message. + if (e is UnimplementedError) { + throw UnimplementedError(''' +This implementation of Shared Preferences doesn't yet support the setPrefix method. +Either update the implementation to support setPrefix, or do not call setPrefix. + '''); + } else { + rethrow; + } + } + } + return _store.clear(); + } + + /// Fetches the latest values from the host platform. + /// + /// Use this method to observe modifications that were made in native code + /// (without using the plugin) while the app is running. + Future reload() async { + final Map preferences = + await SharedPreferences._getSharedPreferencesMap(); + _preferenceCache.clear(); + _preferenceCache.addAll(preferences); + } + + static Future> _getSharedPreferencesMap() async { + final Map fromSystem = {}; + if (_prefixHasBeenChanged) { + try { + fromSystem.addAll( + await _store.getAllWithParameters( + GetAllParameters( + filter: PreferencesFilter( + prefix: _prefix, + allowList: _allowList, + ), + ), + ), + ); + } catch (e) { + // Catching and clarifying UnimplementedError to provide a more robust message. + if (e is UnimplementedError) { + throw UnimplementedError(''' +This implementation of Shared Preferences doesn't yet support the setPrefix method. +Either update the implementation to support setPrefix, or do not call setPrefix. + '''); + } else { + rethrow; + } + } + } else { + fromSystem.addAll(await _store.getAll()); + } + + if (_prefix.isEmpty) { + return fromSystem; + } + // Strip the prefix from the returned preferences. + final Map preferencesMap = {}; + for (final String key in fromSystem.keys) { + assert(key.startsWith(_prefix)); + preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; + } + return preferencesMap; + } + + /// Initializes the shared preferences with mock values for testing. + /// + /// If the singleton instance has been initialized already, it is nullified. + @visibleForTesting + static void setMockInitialValues(Map values) { + final Map newValues = + values.map((String key, Object value) { + String newKey = key; + if (!key.startsWith(_prefix)) { + newKey = '$_prefix$key'; + } + return MapEntry(newKey, value); + }); + SharedPreferencesStorePlatform.instance = + InMemorySharedPreferencesStore.withData(newValues); + _completer = null; + } +} diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index c3775eb455bf..ada0f98ff360 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.2.3 +version: 2.3.0 environment: - sdk: ^3.2.0 - flutter: ">=3.16.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" flutter: plugin: @@ -28,12 +28,12 @@ flutter: dependencies: flutter: sdk: flutter - shared_preferences_android: ^2.1.0 - shared_preferences_foundation: ^2.3.5 - shared_preferences_linux: ^2.2.0 - shared_preferences_platform_interface: ^2.3.0 - shared_preferences_web: ^2.1.0 - shared_preferences_windows: ^2.2.0 + shared_preferences_android: ^2.3.0 + shared_preferences_foundation: ^2.5.0 + shared_preferences_linux: ^2.4.0 + shared_preferences_platform_interface: ^2.4.0 + shared_preferences_web: ^2.4.0 + shared_preferences_windows: ^2.4.0 dev_dependencies: flutter_test: diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_async_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_async_test.dart new file mode 100755 index 000000000000..372b2a5f7992 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_async_test.dart @@ -0,0 +1,919 @@ +// 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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +void main() { + const String stringKey = 'testString'; + const String boolKey = 'testBool'; + const String intKey = 'testInt'; + const String doubleKey = 'testDouble'; + const String listKey = 'testList'; + + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + + group('Async', () { + (SharedPreferencesAsync, FakeSharedPreferencesAsync) getPreferences() { + final FakeSharedPreferencesAsync store = FakeSharedPreferencesAsync(); + SharedPreferencesAsyncPlatform.instance = store; + final SharedPreferencesAsync preferences = SharedPreferencesAsync(); + return (preferences, store); + } + + test('set and get String', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store, + ) = getPreferences(); + await preferences.setString(stringKey, testString); + expect( + store.log, + [ + isMethodCall('setString', arguments: [ + stringKey, + testString, + ]), + ], + ); + store.log.clear(); + expect(await preferences.getString(stringKey), testString); + expect( + store.log, + [ + isMethodCall('getString', arguments: [ + stringKey, + ]), + ], + ); + }); + + test('set and get bool', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await preferences.setBool(boolKey, testBool); + expect( + store.log, + [ + isMethodCall('setBool', arguments: [ + boolKey, + testBool, + ]), + ], + ); + store.log.clear(); + expect(await preferences.getBool(boolKey), testBool); + expect( + store.log, + [ + isMethodCall('getBool', arguments: [ + boolKey, + ]), + ], + ); + }); + + test('set and get int', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await preferences.setInt(intKey, testInt); + expect( + store.log, + [ + isMethodCall('setInt', arguments: [ + intKey, + testInt, + ]), + ], + ); + store.log.clear(); + + expect(await preferences.getInt(intKey), testInt); + expect( + store.log, + [ + isMethodCall('getInt', arguments: [ + intKey, + ]), + ], + ); + }); + + test('set and get double', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await preferences.setDouble(doubleKey, testDouble); + expect( + store.log, + [ + isMethodCall('setDouble', arguments: [ + doubleKey, + testDouble, + ]), + ], + ); + store.log.clear(); + expect(await preferences.getDouble(doubleKey), testDouble); + expect( + store.log, + [ + isMethodCall('getDouble', arguments: [ + doubleKey, + ]), + ], + ); + }); + + test('set and get StringList', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await preferences.setStringList(listKey, testList); + expect( + store.log, + [ + isMethodCall('setStringList', arguments: [ + listKey, + testList, + ]), + ], + ); + store.log.clear(); + expect(await preferences.getStringList(listKey), testList); + expect( + store.log, + [ + isMethodCall('getStringList', arguments: [ + listKey, + ]), + ], + ); + }); + + test('getAll', () async { + final (SharedPreferencesAsync preferences, _) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Map gotAll = await preferences.getAll(); + + expect(gotAll.length, 5); + expect(gotAll[stringKey], testString); + expect(gotAll[boolKey], testBool); + expect(gotAll[intKey], testInt); + expect(gotAll[doubleKey], testDouble); + expect(gotAll[listKey], testList); + }); + + test('getAll with filter', () async { + final (SharedPreferencesAsync preferences, _) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Map gotAll = + await preferences.getAll(allowList: {stringKey, boolKey}); + + expect(gotAll.length, 2); + expect(gotAll[stringKey], testString); + expect(gotAll[boolKey], testBool); + }); + + test('remove', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + const String key = 'testKey'; + await preferences.remove(key); + expect( + store.log, + List.filled( + 1, + isMethodCall( + 'clear', + arguments: [key], + ), + growable: true, + )); + }); + + test('getKeys', () async { + final (SharedPreferencesAsync preferences, _) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = await preferences.getKeys(); + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + test('getKeys with filter', () async { + final (SharedPreferencesAsync preferences, _) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = + await preferences.getKeys(allowList: {stringKey, boolKey}); + + expect(keys.length, 2); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + }); + + test('containsKey', () async { + final (SharedPreferencesAsync preferences, _) = getPreferences(); + const String key = 'testKey'; + + expect(false, await preferences.containsKey(key)); + + await preferences.setString(key, 'test'); + expect(true, await preferences.containsKey(key)); + }); + + test('clear', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + store.log.clear(); + await preferences.clear(); + expect( + store.log, [isMethodCall('clear', arguments: [])]); + expect(await preferences.getString(stringKey), null); + expect(await preferences.getBool(boolKey), null); + expect(await preferences.getInt(intKey), null); + expect(await preferences.getDouble(doubleKey), null); + expect(await preferences.getStringList(listKey), null); + }); + + test('clear with filter', () async { + final ( + SharedPreferencesAsync preferences, + FakeSharedPreferencesAsync store + ) = getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + store.log.clear(); + await preferences.clear(allowList: {stringKey, boolKey}); + expect(store.log, [ + isMethodCall('clear', arguments: [stringKey, boolKey]) + ]); + expect(await preferences.getString(stringKey), null); + expect(await preferences.getBool(boolKey), null); + expect(await preferences.getInt(intKey), testInt); + expect(await preferences.getDouble(doubleKey), testDouble); + expect(await preferences.getStringList(listKey), testList); + }); + }); + + group('withCache', () { + Future< + ( + SharedPreferencesWithCache, + FakeSharedPreferencesAsync, + Map, + )> getPreferences() async { + final Map cache = {}; + final FakeSharedPreferencesAsync store = FakeSharedPreferencesAsync(); + SharedPreferencesAsyncPlatform.instance = store; + final SharedPreferencesWithCache preferences = + await SharedPreferencesWithCache.create( + cache: cache, + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); + store.log.clear(); + return (preferences, store, cache); + } + + test('set and get String', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setString(stringKey, testString); + expect( + store.log, + [ + isMethodCall('setString', arguments: [ + stringKey, + testString, + ]), + ], + ); + store.log.clear(); + expect(preferences.getString(stringKey), testString); + expect( + store.log, + [], + ); + }); + + test('set and get bool', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setBool(boolKey, testBool); + expect( + store.log, + [ + isMethodCall('setBool', arguments: [ + boolKey, + testBool, + ]), + ], + ); + store.log.clear(); + expect(preferences.getBool(boolKey), testBool); + expect( + store.log, + [], + ); + }); + + test('set and get int', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setInt(intKey, testInt); + expect( + store.log, + [ + isMethodCall('setInt', arguments: [ + intKey, + testInt, + ]), + ], + ); + store.log.clear(); + + expect(preferences.getInt(intKey), testInt); + expect( + store.log, + [], + ); + }); + + test('set and get double', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setDouble(doubleKey, testDouble); + expect( + store.log, + [ + isMethodCall('setDouble', arguments: [ + doubleKey, + testDouble, + ]), + ], + ); + store.log.clear(); + expect(preferences.getDouble(doubleKey), testDouble); + expect( + store.log, + [], + ); + }); + + test('set and get StringList', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setStringList(listKey, testList); + expect( + store.log, + [ + isMethodCall('setStringList', arguments: [ + listKey, + testList, + ]), + ], + ); + store.log.clear(); + expect(preferences.getStringList(listKey), testList); + expect( + store.log, + [], + ); + }); + + test('reloading', () async { + final ( + SharedPreferencesWithCache preferences, + _, + Map cache, + ) = await getPreferences(); + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + + cache.clear(); + expect(preferences.getString(stringKey), null); + + await preferences.reloadCache(); + expect(preferences.getString(stringKey), testString); + }); + + test('containsKey', () async { + final ( + SharedPreferencesWithCache preferences, + _, + _, + ) = await getPreferences(); + const String key = 'testKey'; + + expect(false, preferences.containsKey(key)); + + await preferences.setString(key, 'test'); + expect(true, preferences.containsKey(key)); + }); + + test('getKeys', () async { + final ( + SharedPreferencesWithCache preferences, + _, + _, + ) = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = preferences.keys; + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + test('remove', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + const String key = 'testKey'; + await preferences.remove(key); + expect( + store.log, + List.filled( + 1, + isMethodCall( + 'clear', + arguments: [key], + ), + growable: true, + )); + }); + + test('clear', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + store.log.clear(); + await preferences.clear(); + expect( + store.log, [isMethodCall('clear', arguments: [])]); + expect(preferences.getString(stringKey), null); + expect(preferences.getBool(boolKey), null); + expect(preferences.getInt(intKey), null); + expect(preferences.getDouble(doubleKey), null); + expect(preferences.getStringList(listKey), null); + }); + }); + + group('withCache with filter', () { + Future< + ( + SharedPreferencesWithCache, + FakeSharedPreferencesAsync, + Map, + )> getPreferences() async { + final Map cache = {}; + final FakeSharedPreferencesAsync store = FakeSharedPreferencesAsync(); + SharedPreferencesAsyncPlatform.instance = store; + final SharedPreferencesWithCache preferences = + await SharedPreferencesWithCache.create( + cache: cache, + cacheOptions: + const SharedPreferencesWithCacheOptions(allowList: { + stringKey, + boolKey, + intKey, + doubleKey, + listKey, + }), + ); + store.log.clear(); + return (preferences, store, cache); + } + + test('set and get String', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setString(stringKey, testString); + expect( + store.log, + [ + isMethodCall('setString', arguments: [ + stringKey, + testString, + ]), + ], + ); + store.log.clear(); + expect(preferences.getString(stringKey), testString); + expect( + store.log, + [], + ); + }); + + test('set and get bool', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setBool(boolKey, testBool); + expect( + store.log, + [ + isMethodCall('setBool', arguments: [ + boolKey, + testBool, + ]), + ], + ); + store.log.clear(); + expect(preferences.getBool(boolKey), testBool); + expect( + store.log, + [], + ); + }); + + test('set and get int', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setInt(intKey, testInt); + expect( + store.log, + [ + isMethodCall('setInt', arguments: [ + intKey, + testInt, + ]), + ], + ); + store.log.clear(); + + expect(preferences.getInt(intKey), testInt); + expect( + store.log, + [], + ); + }); + + test('set and get double', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setDouble(doubleKey, testDouble); + expect( + store.log, + [ + isMethodCall('setDouble', arguments: [ + doubleKey, + testDouble, + ]), + ], + ); + store.log.clear(); + expect(preferences.getDouble(doubleKey), testDouble); + expect( + store.log, + [], + ); + }); + + test('set and get StringList', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.setStringList(listKey, testList); + expect( + store.log, + [ + isMethodCall('setStringList', arguments: [ + listKey, + testList, + ]), + ], + ); + store.log.clear(); + expect(preferences.getStringList(listKey), testList); + expect( + store.log, + [], + ); + }); + test('reloading', () async { + final ( + SharedPreferencesWithCache preferences, + _, + Map cache, + ) = await getPreferences(); + await preferences.setString(stringKey, testString); + expect(preferences.getString(stringKey), testString); + + cache.clear(); + expect(preferences.getString(stringKey), null); + + await preferences.reloadCache(); + expect(preferences.getString(stringKey), testString); + }); + + test('throws ArgumentError if key is not included in filter', () async { + final ( + SharedPreferencesWithCache preferences, + _, + _, + ) = await getPreferences(); + const String key = 'testKey'; + + expect( + () async => preferences.setString(key, 'test'), throwsArgumentError); + }); + + test('containsKey', () async { + final ( + SharedPreferencesWithCache preferences, + _, + _, + ) = await getPreferences(); + + expect(false, preferences.containsKey(stringKey)); + + await preferences.setString(stringKey, 'test'); + expect(true, preferences.containsKey(stringKey)); + }); + + test('getKeys', () async { + final ( + SharedPreferencesWithCache preferences, + _, + _, + ) = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + + final Set keys = preferences.keys; + + expect(keys.length, 5); + expect(keys, contains(stringKey)); + expect(keys, contains(boolKey)); + expect(keys, contains(intKey)); + expect(keys, contains(doubleKey)); + expect(keys, contains(listKey)); + }); + + test('remove', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await preferences.remove(stringKey); + expect( + store.log, + List.filled( + 1, + isMethodCall( + 'clear', + arguments: [stringKey], + ), + growable: true, + )); + }); + + test('clear', () async { + final ( + SharedPreferencesWithCache preferences, + FakeSharedPreferencesAsync store, + _, + ) = await getPreferences(); + await Future.wait(>[ + preferences.setString(stringKey, testString), + preferences.setBool(boolKey, testBool), + preferences.setInt(intKey, testInt), + preferences.setDouble(doubleKey, testDouble), + preferences.setStringList(listKey, testList) + ]); + store.log.clear(); + await preferences.clear(); + expect(store.log, [ + isMethodCall('clear', arguments: [ + stringKey, + boolKey, + intKey, + doubleKey, + listKey, + ]) + ]); + + expect(preferences.getString(stringKey), null); + expect(preferences.getBool(boolKey), null); + // The cache will clear everything, even though the backend will still hold this data. + // Since the cache shouldn't ever be able to add data that isn't in the allowlist, + // this is expected behavior. + expect(preferences.getInt(intKey), null); + expect(preferences.getDouble(doubleKey), null); + expect(preferences.getStringList(listKey), null); + }); + }); +} + +base class FakeSharedPreferencesAsync extends SharedPreferencesAsyncPlatform { + final InMemorySharedPreferencesAsync backend = + InMemorySharedPreferencesAsync.empty(); + final List log = []; + + @override + Future clear( + ClearPreferencesParameters parameters, SharedPreferencesOptions options) { + log.add(MethodCall('clear', [...?parameters.filter.allowList])); + return backend.clear(parameters, options); + } + + @override + Future getBool(String key, SharedPreferencesOptions options) { + log.add(MethodCall('getBool', [key])); + return backend.getBool(key, options); + } + + @override + Future getDouble(String key, SharedPreferencesOptions options) { + log.add(MethodCall('getDouble', [key])); + return backend.getDouble(key, options); + } + + @override + Future getInt(String key, SharedPreferencesOptions options) { + log.add(MethodCall('getInt', [key])); + return backend.getInt(key, options); + } + + @override + Future> getKeys( + GetPreferencesParameters parameters, SharedPreferencesOptions options) { + log.add(MethodCall('getKeys', [...?parameters.filter.allowList])); + return backend.getKeys(parameters, options); + } + + @override + Future> getPreferences( + GetPreferencesParameters parameters, SharedPreferencesOptions options) { + log.add(MethodCall( + 'getPreferences', [...?parameters.filter.allowList])); + return backend.getPreferences(parameters, options); + } + + @override + Future getString(String key, SharedPreferencesOptions options) { + log.add(MethodCall('getString', [key])); + return backend.getString(key, options); + } + + @override + Future?> getStringList( + String key, SharedPreferencesOptions options) { + log.add(MethodCall('getStringList', [key])); + return backend.getStringList(key, options); + } + + @override + Future setBool( + String key, bool value, SharedPreferencesOptions options) { + log.add(MethodCall('setBool', [key, value])); + return backend.setBool(key, value, options); + } + + @override + Future setDouble( + String key, double value, SharedPreferencesOptions options) { + log.add(MethodCall('setDouble', [key, value])); + return backend.setDouble(key, value, options); + } + + @override + Future setInt(String key, int value, SharedPreferencesOptions options) { + log.add(MethodCall('setInt', [key, value])); + return backend.setInt(key, value, options); + } + + @override + Future setString( + String key, String value, SharedPreferencesOptions options) { + log.add(MethodCall('setString', [key, value])); + return backend.setString(key, value, options); + } + + @override + Future setStringList( + String key, List value, SharedPreferencesOptions options) { + log.add(MethodCall('setStringList', [key, value])); + return backend.setStringList(key, value, options); + } +} diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index f7f5ea355d1f..275b3ca7f7a7 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -28,7 +28,7 @@ void main() { const bool testBool2 = false; const int testInt2 = 1337; const double testDouble2 = 2.71828; - const List testList2 = ['baz', 'quox']; + const List testList2 = ['baz', 'qux']; const Map testValues2 = { 'flutter.String': testString2, 'flutter.bool': testBool2, @@ -319,23 +319,25 @@ void main() { "Shared Preferences doesn't yet support the setPrefix method")); }); - test('non-Unimplemented errors pass through withParameters methods correctly', - () async { - final ThrowingSharedPreferencesStore localStore = - ThrowingSharedPreferencesStore(); - SharedPreferencesStorePlatform.instance = localStore; - SharedPreferences.resetStatic(); - SharedPreferences.setPrefix(''); - Object? err; - - try { - await SharedPreferences.getInstance(); - } catch (e) { - err = e; - } - expect(err, isA()); - expect(err.toString(), contains('State Error')); - }); + test( + 'non-Unimplemented errors pass through withParameters methods correctly', + () async { + final ThrowingSharedPreferencesStore localStore = + ThrowingSharedPreferencesStore(); + SharedPreferencesStorePlatform.instance = localStore; + SharedPreferences.resetStatic(); + SharedPreferences.setPrefix(''); + Object? err; + + try { + await SharedPreferences.getInstance(); + } catch (e) { + err = e; + } + expect(err, isA()); + expect(err.toString(), contains('State Error')); + }, + ); } class FakeSharedPreferencesStore extends SharedPreferencesStorePlatform { diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index b611dd3cdc03..8c34e536f394 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.1 + +* Adds comments about unsupported types. + ## 2.4.0 * Adds `SharedPreferencesAsyncPlatform` API. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_async_platform_interface.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_async_platform_interface.dart index 559483b83647..577a9db6234d 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_async_platform_interface.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_async_platform_interface.dart @@ -52,6 +52,7 @@ abstract base class SharedPreferencesAsyncPlatform { /// Retrieves the String [value] associated with the [key], if any. /// /// Throws a [TypeError] if the returned type is not a String. + /// May return null for unsupported types. Future getString( String key, SharedPreferencesOptions options, @@ -60,6 +61,7 @@ abstract base class SharedPreferencesAsyncPlatform { /// Retrieves the bool [value] associated with the [key], if any. /// /// Throws a [TypeError] if the returned type is not a bool. + /// May return null for unsupported types. Future getBool( String key, SharedPreferencesOptions options, @@ -68,6 +70,7 @@ abstract base class SharedPreferencesAsyncPlatform { /// Retrieves the double [value] associated with the [key], if any. /// /// Throws a [TypeError] if the returned type is not a double. + /// May return null for unsupported types. Future getDouble( String key, SharedPreferencesOptions options, @@ -76,6 +79,7 @@ abstract base class SharedPreferencesAsyncPlatform { /// Retrieves the int [value] associated with the [key], if any. /// /// Throws a [TypeError] if the returned type is not an int. + /// May return null for unsupported types. Future getInt( String key, SharedPreferencesOptions options, @@ -84,6 +88,7 @@ abstract base class SharedPreferencesAsyncPlatform { /// Retrieves the List [value] associated with the [key], if any. /// /// Throws a [TypeError] if the returned type is not a List. + /// May return null for unsupported types. Future?> getStringList( String key, SharedPreferencesOptions options, @@ -96,12 +101,17 @@ abstract base class SharedPreferencesAsyncPlatform { ); /// Returns all key/value pairs persisting in this store that match the given [parameters]. + /// + /// Does not return unsupported types, or lists containing unsupported types. Future> getPreferences( GetPreferencesParameters parameters, SharedPreferencesOptions options, ); /// Returns all keys persisting in this store that match the given [parameters]. + /// + /// Does not return keys for values that are unsupported types, or lists containing + /// unsupported types. Future> getKeys( GetPreferencesParameters parameters, SharedPreferencesOptions options, diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 8d909560b0e4..c70c72922434 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_platform_interface description: A common platform interface for the shared_preferences plugin. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.4.0 +version: 2.4.1 environment: sdk: ^3.2.0