From 7407d462a986015443b8eb89cee931738587ad13 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Fri, 13 Dec 2024 14:27:05 -0800 Subject: [PATCH 1/6] adds migration tool and tests --- .../shared_preferences/CHANGELOG.md | 5 + ...hared_preferences_migration_tool_test.dart | 232 ++++++++++++++++++ .../shared_preferences/example/pubspec.yaml | 4 + .../lib/src/shared_preferences_legacy.dart | 3 + .../tools/legacy_to_async_migration_tool.dart | 67 +++++ .../shared_preferences/pubspec.yaml | 2 +- 6 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart create mode 100644 packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 81708ac6532..a144fc2bff0 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.3.5 + +* Adds clarifying comment about allowList handling with an updated prefix. +* Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`. + ## 2.3.3 * Clarifies scope of prefix handling in README. diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart new file mode 100644 index 00000000000..714360822a4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart @@ -0,0 +1,232 @@ +// 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:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences/tools/legacy_to_async_migration_tool.dart'; +import 'package:shared_preferences_android/shared_preferences_android.dart'; +import 'package:shared_preferences_foundation/shared_preferences_foundation.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + 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('shared_preferences', () { + late SharedPreferences preferences; + late SharedPreferencesOptions sharedPreferencesAsyncOptions; + const String migrationCompletedKey = 'migrationCompleted'; + + void runTests( + {String? stringValue = testString, bool keysAndNamesCollide = false}) { + testWidgets('data is successfully transferred to new system', (_) async { + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + expect(await asyncPreferences.getBool(boolKey), testBool); + expect(await asyncPreferences.getInt(intKey), testInt); + expect(await asyncPreferences.getDouble(doubleKey), testDouble); + expect(await asyncPreferences.getString(stringKey), stringValue); + expect(await asyncPreferences.getStringList(listKey), testList); + }); + + testWidgets('migrationCompleted key is set', (_) async { + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + expect(await asyncPreferences.getBool(migrationCompletedKey), true); + }); + + testWidgets( + 're-running migration tool does not overwrite data', + (_) async { + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + await preferences.setInt(intKey, -0); + await migrateLegacySharedPreferencesToSharedPreferencesAsync( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + expect(await asyncPreferences.getInt(intKey), testInt); + }, + // Since the desktop versions would be moving to the same file, this test will always fail. + // They are the same files with the same keys. + skip: keysAndNamesCollide && + (Platform.isWindows || + Platform.isLinux || + Platform.isMacOS || + Platform.isIOS), + ); + } + + void runAllGroups( + {String? stringValue = testString, bool keysCollide = false}) { + setUp(() async { + await preferences.setBool(boolKey, testBool); + await preferences.setInt(intKey, testInt); + await preferences.setDouble(doubleKey, testDouble); + await preferences.setString(stringKey, testString); + await preferences.setStringList(listKey, testList); + }); + group('default sharedPreferencesAsyncOptions', () { + setUp(() async { + sharedPreferencesAsyncOptions = const SharedPreferencesOptions(); + + await migrateLegacySharedPreferencesToSharedPreferencesAsync( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + }); + + tearDown(() async { + await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) + .clear(); + }); + group('', () { + runTests(stringValue: stringValue, keysAndNamesCollide: keysCollide); + }); + }); + + group('file name (or equivalent) sharedPreferencesAsyncOptions', () { + setUp(() async { + if (Platform.isAndroid) { + sharedPreferencesAsyncOptions = + const SharedPreferencesAsyncAndroidOptions( + backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, + originalSharedPreferencesOptions: + AndroidSharedPreferencesStoreOptions( + fileName: 'fileName', + ), + ); + } else if (Platform.isIOS || Platform.isMacOS) { + sharedPreferencesAsyncOptions = + SharedPreferencesAsyncFoundationOptions( + suiteName: 'group.fileName'); + } else if (Platform.isLinux) { + sharedPreferencesAsyncOptions = const SharedPreferencesLinuxOptions( + fileName: 'fileName', + ); + } else if (Platform.isWindows) { + sharedPreferencesAsyncOptions = + const SharedPreferencesWindowsOptions(fileName: 'fileName'); + } else { + sharedPreferencesAsyncOptions = const SharedPreferencesOptions(); + } + + await migrateLegacySharedPreferencesToSharedPreferencesAsync( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + }); + + tearDown(() async { + await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) + .clear(); + }); + group('', () { + runTests(stringValue: stringValue); + }); + }); + + if (Platform.isAndroid) { + group('Android default sharedPreferences', () { + setUp(() async { + sharedPreferencesAsyncOptions = + const SharedPreferencesAsyncAndroidOptions( + backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, + originalSharedPreferencesOptions: + AndroidSharedPreferencesStoreOptions(), + ); + + await migrateLegacySharedPreferencesToSharedPreferencesAsync( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + }); + + tearDown(() async { + await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) + .clear(); + }); + group('', () { + runTests(stringValue: stringValue); + }); + }); + } + } + + group('SharedPreferences without setting prefix', () { + setUp(() async { + SharedPreferences.resetStatic(); + preferences = await SharedPreferences.getInstance(); + await preferences.clear(); + group('', () { + runAllGroups(); + }); + }); + }); + + group('SharedPreferences with setPrefix', () { + setUp(() async { + SharedPreferences.resetStatic(); + SharedPreferences.setPrefix('prefix.'); + preferences = await SharedPreferences.getInstance(); + await preferences.clear(); + }); + group('', () { + runAllGroups(); + }); + }); + + group('SharedPreferences with setPrefix and allowList', () { + setUp(() async { + SharedPreferences.resetStatic(); + final Set allowList = { + 'prefix.$boolKey', + 'prefix.$intKey', + 'prefix.$doubleKey', + 'prefix.$listKey' + }; + SharedPreferences.setPrefix('prefix.', allowList: allowList); + preferences = await SharedPreferences.getInstance(); + await preferences.clear(); + }); + group('', () { + runAllGroups(stringValue: null); + }); + }); + + group('SharedPreferences with prefix set to empty string', () { + setUp(() async { + SharedPreferences.resetStatic(); + SharedPreferences.setPrefix(''); + preferences = await SharedPreferences.getInstance(); + await preferences.clear(); + }); + group('', () { + runAllGroups(keysCollide: true); + }); + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index cbf79d353af..f8f4b195744 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -16,7 +16,11 @@ 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_android: ^2.4.0 + shared_preferences_foundation: ^2.5.3 + shared_preferences_linux: ^2.4.1 shared_preferences_platform_interface: ^2.4.0 + shared_preferences_windows: ^2.4.1 dev_dependencies: build_runner: ^2.1.10 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 index 72deffe5fe9..17ef35e1103 100644 --- a/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart +++ b/packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart @@ -44,6 +44,9 @@ class SharedPreferences { /// [allowList] will cause the plugin to only return preferences that /// are both contained in the list AND match the provided prefix. /// + /// If [prefix] is changed, and an [allowList] is used, the prefix must be included + /// on the keys added to the [allowList]. + /// /// 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. diff --git a/packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart b/packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart new file mode 100644 index 00000000000..7948af86eef --- /dev/null +++ b/packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart @@ -0,0 +1,67 @@ +// 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:shared_preferences_platform_interface/types.dart'; + +import '../shared_preferences.dart'; + +/// A tool to migrate from the legacy SharedPreferences system to +/// SharedPreferencesAsync. +/// +/// [legacySharedPreferencesInstance] should be an instance of [SharedPreferences] +/// that has been instantiated the same way it has been used throughout your app. +/// If you have called [SharedPreferences.setPrefix] that must be done before using +/// this tool. +/// +/// [sharedPreferencesAsyncOptions] should be an instance of [SharedPreferencesOptions] +/// that is set up the way you intend to use the new system going forward. +/// This tool will allow for future use of [SharedPreferencesAsync] and [SharedPreferencesWithCache]. +/// +/// The [migrationCompletedKey] is a key that will be used to check if the migration +/// has run before, to avoid overwriting new data going forward. Make sure that +/// there will not be any collisions with preferences you are or will be setting +/// going forward, or there may be data loss. +Future migrateLegacySharedPreferencesToSharedPreferencesAsync( + SharedPreferences legacySharedPreferencesInstance, + SharedPreferencesOptions sharedPreferencesAsyncOptions, + String migrationCompletedKey, { + bool clearLegacyPreferences = false, +}) async { + final SharedPreferencesAsync sharedPreferencesAsyncInstance = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + if (await sharedPreferencesAsyncInstance.containsKey(migrationCompletedKey)) { + return; + } + + Set keys = legacySharedPreferencesInstance.getKeys(); + await legacySharedPreferencesInstance.reload(); + keys = legacySharedPreferencesInstance.getKeys(); + + for (final String key in keys) { + final Object? value = legacySharedPreferencesInstance.get(key); + switch (value.runtimeType) { + case const (bool): + await sharedPreferencesAsyncInstance.setBool(key, value! as bool); + case const (int): + await sharedPreferencesAsyncInstance.setInt(key, value! as int); + case const (double): + await sharedPreferencesAsyncInstance.setDouble(key, value! as double); + case const (String): + await sharedPreferencesAsyncInstance.setString(key, value! as String); + case const (List): + case const (List): + case const (List): + case const (List): + try { + await sharedPreferencesAsyncInstance.setStringList( + key, (value! as List).cast()); + } catch (_) {} // Pass over Lists containing non-String values. + } + } + + await sharedPreferencesAsyncInstance.setBool(migrationCompletedKey, true); + + return; +} diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 6c715ff7e3a..c22ace523aa 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. 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.3.3 +version: 2.3.5 environment: sdk: ^3.4.0 From 229944da4c8cceeec7310ccc7d0b1141acbac6c1 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Tue, 7 Jan 2025 11:20:48 -0800 Subject: [PATCH 2/6] comments and test structure --- .../shared_preferences/CHANGELOG.md | 4 +- ...hared_preferences_migration_tool_test.dart | 380 +++++++++--------- .../legacy_to_async_migration_util.dart} | 25 +- .../shared_preferences/pubspec.yaml | 2 +- 4 files changed, 197 insertions(+), 214 deletions(-) rename packages/shared_preferences/shared_preferences/lib/{tools/legacy_to_async_migration_tool.dart => util/legacy_to_async_migration_util.dart} (74%) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index f2138015ca4..bf38c333b94 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,6 +1,6 @@ -## 2.3.6 +## 2.4.0 -* Adds clarifying comment about allowList handling with an updated prefix. +* Adds clarifying comment about `allowList` handling with an updated prefix. * Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`. ## 2.3.5 diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart index 714360822a4..cf67fab8d3b 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart @@ -7,201 +7,43 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:shared_preferences/tools/legacy_to_async_migration_tool.dart'; +import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; import 'package:shared_preferences_android/shared_preferences_android.dart'; import 'package:shared_preferences_foundation/shared_preferences_foundation.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; import 'package:shared_preferences_platform_interface/types.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +const String stringKey = 'testString'; +const String boolKey = 'testBool'; +const String intKey = 'testInt'; +const String doubleKey = 'testDouble'; +const String listKey = 'testList'; - 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('shared_preferences', () { - late SharedPreferences preferences; - late SharedPreferencesOptions sharedPreferencesAsyncOptions; - const String migrationCompletedKey = 'migrationCompleted'; - - void runTests( - {String? stringValue = testString, bool keysAndNamesCollide = false}) { - testWidgets('data is successfully transferred to new system', (_) async { - final SharedPreferencesAsync asyncPreferences = - SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); - - expect(await asyncPreferences.getBool(boolKey), testBool); - expect(await asyncPreferences.getInt(intKey), testInt); - expect(await asyncPreferences.getDouble(doubleKey), testDouble); - expect(await asyncPreferences.getString(stringKey), stringValue); - expect(await asyncPreferences.getStringList(listKey), testList); - }); - - testWidgets('migrationCompleted key is set', (_) async { - final SharedPreferencesAsync asyncPreferences = - SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); - - expect(await asyncPreferences.getBool(migrationCompletedKey), true); - }); - - testWidgets( - 're-running migration tool does not overwrite data', - (_) async { - final SharedPreferencesAsync asyncPreferences = - SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); - await preferences.setInt(intKey, -0); - await migrateLegacySharedPreferencesToSharedPreferencesAsync( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, - ); - expect(await asyncPreferences.getInt(intKey), testInt); - }, - // Since the desktop versions would be moving to the same file, this test will always fail. - // They are the same files with the same keys. - skip: keysAndNamesCollide && - (Platform.isWindows || - Platform.isLinux || - Platform.isMacOS || - Platform.isIOS), - ); - } +const String testString = 'hello world'; +const bool testBool = true; +const int testInt = 42; +const double testDouble = 3.14159; +const List testList = ['foo', 'bar']; - void runAllGroups( - {String? stringValue = testString, bool keysCollide = false}) { - setUp(() async { - await preferences.setBool(boolKey, testBool); - await preferences.setInt(intKey, testInt); - await preferences.setDouble(doubleKey, testDouble); - await preferences.setString(stringKey, testString); - await preferences.setStringList(listKey, testList); - }); - group('default sharedPreferencesAsyncOptions', () { - setUp(() async { - sharedPreferencesAsyncOptions = const SharedPreferencesOptions(); - - await migrateLegacySharedPreferencesToSharedPreferencesAsync( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, - ); - }); - - tearDown(() async { - await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) - .clear(); - }); - group('', () { - runTests(stringValue: stringValue, keysAndNamesCollide: keysCollide); - }); - }); - - group('file name (or equivalent) sharedPreferencesAsyncOptions', () { - setUp(() async { - if (Platform.isAndroid) { - sharedPreferencesAsyncOptions = - const SharedPreferencesAsyncAndroidOptions( - backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, - originalSharedPreferencesOptions: - AndroidSharedPreferencesStoreOptions( - fileName: 'fileName', - ), - ); - } else if (Platform.isIOS || Platform.isMacOS) { - sharedPreferencesAsyncOptions = - SharedPreferencesAsyncFoundationOptions( - suiteName: 'group.fileName'); - } else if (Platform.isLinux) { - sharedPreferencesAsyncOptions = const SharedPreferencesLinuxOptions( - fileName: 'fileName', - ); - } else if (Platform.isWindows) { - sharedPreferencesAsyncOptions = - const SharedPreferencesWindowsOptions(fileName: 'fileName'); - } else { - sharedPreferencesAsyncOptions = const SharedPreferencesOptions(); - } - - await migrateLegacySharedPreferencesToSharedPreferencesAsync( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, - ); - }); - - tearDown(() async { - await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) - .clear(); - }); - group('', () { - runTests(stringValue: stringValue); - }); - }); - - if (Platform.isAndroid) { - group('Android default sharedPreferences', () { - setUp(() async { - sharedPreferencesAsyncOptions = - const SharedPreferencesAsyncAndroidOptions( - backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, - originalSharedPreferencesOptions: - AndroidSharedPreferencesStoreOptions(), - ); - - await migrateLegacySharedPreferencesToSharedPreferencesAsync( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, - ); - }); - - tearDown(() async { - await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) - .clear(); - }); - group('', () { - runTests(stringValue: stringValue); - }); - }); - } - } +const String migrationCompletedKey = 'migrationCompleted'; - group('SharedPreferences without setting prefix', () { - setUp(() async { - SharedPreferences.resetStatic(); - preferences = await SharedPreferences.getInstance(); - await preferences.clear(); - group('', () { - runAllGroups(); - }); - }); - }); +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('SharedPreferences with setPrefix', () { - setUp(() async { - SharedPreferences.resetStatic(); - SharedPreferences.setPrefix('prefix.'); - preferences = await SharedPreferences.getInstance(); - await preferences.clear(); - }); - group('', () { - runAllGroups(); - }); + group('SharedPreferences without setting prefix', () { + runAllGroups(() {}); + }); + + group('SharedPreferences with setPrefix', () { + runAllGroups(() { + SharedPreferences.setPrefix('prefix.'); }); + }); - group('SharedPreferences with setPrefix and allowList', () { - setUp(() async { - SharedPreferences.resetStatic(); + group('SharedPreferences with setPrefix and allowList', () { + runAllGroups( + () { final Set allowList = { 'prefix.$boolKey', 'prefix.$intKey', @@ -209,24 +51,164 @@ void main() { 'prefix.$listKey' }; SharedPreferences.setPrefix('prefix.', allowList: allowList); - preferences = await SharedPreferences.getInstance(); - await preferences.clear(); - }); - group('', () { - runAllGroups(stringValue: null); - }); - }); + }, + stringValue: null, + ); + }); - group('SharedPreferences with prefix set to empty string', () { - setUp(() async { - SharedPreferences.resetStatic(); + group('SharedPreferences with prefix set to empty string', () { + runAllGroups( + () { SharedPreferences.setPrefix(''); - preferences = await SharedPreferences.getInstance(); - await preferences.clear(); - }); - group('', () { - runAllGroups(keysCollide: true); - }); + }, + keysCollide: true, + ); + }); +} + +void runAllGroups(void Function() legacySharedPrefsConfig, + {String? stringValue = testString, bool keysCollide = false}) { + group('default sharedPreferencesAsyncOptions', () { + const SharedPreferencesOptions sharedPreferencesAsyncOptions = + SharedPreferencesOptions(); + + runTests( + sharedPreferencesAsyncOptions, + legacySharedPrefsConfig, + stringValue: stringValue, + keysAndNamesCollide: keysCollide, + ); + }); + + group('file name (or equivalent) sharedPreferencesAsyncOptions', () { + final SharedPreferencesOptions sharedPreferencesAsyncOptions; + if (Platform.isAndroid) { + sharedPreferencesAsyncOptions = + const SharedPreferencesAsyncAndroidOptions( + backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, + originalSharedPreferencesOptions: AndroidSharedPreferencesStoreOptions( + fileName: 'fileName', + ), + ); + } else if (Platform.isIOS || Platform.isMacOS) { + sharedPreferencesAsyncOptions = + SharedPreferencesAsyncFoundationOptions(suiteName: 'group.fileName'); + } else if (Platform.isLinux) { + sharedPreferencesAsyncOptions = const SharedPreferencesLinuxOptions( + fileName: 'fileName', + ); + } else if (Platform.isWindows) { + sharedPreferencesAsyncOptions = + const SharedPreferencesWindowsOptions(fileName: 'fileName'); + } else { + sharedPreferencesAsyncOptions = const SharedPreferencesOptions(); + } + + runTests( + sharedPreferencesAsyncOptions, + legacySharedPrefsConfig, + stringValue: stringValue, + ); + }); + + if (Platform.isAndroid) { + group('Android default sharedPreferences', () { + const SharedPreferencesOptions sharedPreferencesAsyncOptions = + SharedPreferencesAsyncAndroidOptions( + backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, + originalSharedPreferencesOptions: + AndroidSharedPreferencesStoreOptions(), + ); + + runTests( + sharedPreferencesAsyncOptions, + legacySharedPrefsConfig, + stringValue: stringValue, + ); }); + } +} + +void runTests(SharedPreferencesOptions sharedPreferencesAsyncOptions, + void Function() legacySharedPrefsConfig, + {String? stringValue = testString, bool keysAndNamesCollide = false}) { + setUp(() async { + // Configure and populate the source legacy shared preferences. + SharedPreferences.resetStatic(); + legacySharedPrefsConfig(); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.clear(); + await preferences.setBool(boolKey, testBool); + await preferences.setInt(intKey, testInt); + await preferences.setDouble(doubleKey, testDouble); + await preferences.setString(stringKey, testString); + await preferences.setStringList(listKey, testList); + }); + + tearDown(() async { + await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions) + .clear(); }); + + testWidgets('data is successfully transferred to new system', (_) async { + final SharedPreferences preferences = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + expect(await asyncPreferences.getBool(boolKey), testBool); + expect(await asyncPreferences.getInt(intKey), testInt); + expect(await asyncPreferences.getDouble(doubleKey), testDouble); + expect(await asyncPreferences.getString(stringKey), stringValue); + expect(await asyncPreferences.getStringList(listKey), testList); + }); + + testWidgets('migrationCompleted key is set', (_) async { + final SharedPreferences preferences = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + expect(await asyncPreferences.getBool(migrationCompletedKey), true); + }); + + testWidgets( + 're-running migration tool does not overwrite data', + (_) async { + final SharedPreferences preferences = + await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + await preferences.setInt(intKey, -0); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + preferences, + sharedPreferencesAsyncOptions, + migrationCompletedKey, + ); + expect(await asyncPreferences.getInt(intKey), testInt); + }, + // Skips platforms that would be adding the preferences to the same file. + skip: keysAndNamesCollide && + (Platform.isWindows || + Platform.isLinux || + Platform.isMacOS || + Platform.isIOS), + ); } diff --git a/packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart similarity index 74% rename from packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart rename to packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart index 7948af86eef..b3d7161014a 100644 --- a/packages/shared_preferences/shared_preferences/lib/tools/legacy_to_async_migration_tool.dart +++ b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart @@ -6,9 +6,12 @@ import 'package:shared_preferences_platform_interface/types.dart'; import '../shared_preferences.dart'; -/// A tool to migrate from the legacy SharedPreferences system to +/// Migrates preferences from the legacy SharedPreferences system to /// SharedPreferencesAsync. /// +/// This method can be run multiple times without worry of overwriting transferred data, +/// as long as the [migrationCompletedKey] is not altered manually. +/// /// [legacySharedPreferencesInstance] should be an instance of [SharedPreferences] /// that has been instantiated the same way it has been used throughout your app. /// If you have called [SharedPreferences.setPrefix] that must be done before using @@ -18,16 +21,15 @@ import '../shared_preferences.dart'; /// that is set up the way you intend to use the new system going forward. /// This tool will allow for future use of [SharedPreferencesAsync] and [SharedPreferencesWithCache]. /// -/// The [migrationCompletedKey] is a key that will be used to check if the migration -/// has run before, to avoid overwriting new data going forward. Make sure that -/// there will not be any collisions with preferences you are or will be setting -/// going forward, or there may be data loss. -Future migrateLegacySharedPreferencesToSharedPreferencesAsync( +/// The [migrationCompletedKey] is a key that is stored in the target preferences +/// which is used to check if the migration has run before, to avoid overwriting +/// new data going forward. Make sure that there will not be any collisions with +/// preferences you are or will be setting going forward, or there may be data loss. +Future migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( SharedPreferences legacySharedPreferencesInstance, SharedPreferencesOptions sharedPreferencesAsyncOptions, - String migrationCompletedKey, { - bool clearLegacyPreferences = false, -}) async { + String migrationCompletedKey, +) async { final SharedPreferencesAsync sharedPreferencesAsyncInstance = SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); @@ -35,9 +37,8 @@ Future migrateLegacySharedPreferencesToSharedPreferencesAsync( return; } - Set keys = legacySharedPreferencesInstance.getKeys(); await legacySharedPreferencesInstance.reload(); - keys = legacySharedPreferencesInstance.getKeys(); + final Set keys = legacySharedPreferencesInstance.getKeys(); for (final String key in keys) { final Object? value = legacySharedPreferencesInstance.get(key); @@ -57,7 +58,7 @@ Future migrateLegacySharedPreferencesToSharedPreferencesAsync( try { await sharedPreferencesAsyncInstance.setStringList( key, (value! as List).cast()); - } catch (_) {} // Pass over Lists containing non-String values. + } on TypeError catch (_) {} // Pass over Lists containing non-String values. } } diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 7a16c3ba0fb..e292865c1c7 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. 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.3.6 +version: 2.4.0 environment: sdk: ^3.5.0 From 9b3e6a54f606e0ca82c96fb2cb1d1725bdd94293 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Mon, 13 Jan 2025 11:39:48 -0800 Subject: [PATCH 3/6] readme --- .../shared_preferences/README.md | 24 +++++++++++-------- .../shared_preferences/example/lib/main.dart | 23 ++++++++++++++---- .../util/legacy_to_async_migration_util.dart | 7 +++--- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index ad6c7268eed..3583618cbeb 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -161,18 +161,22 @@ await prefsWithCache.clear(); #### 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. +Migrating to the newer `SharedPreferencesAsync` or `SharedPreferencesWithCache` API's is straightforward. +All that is need is to import the migration utility, and provide it with the `SharedPreferences` instance +that was being used previously, as well as the options for the desired new API options. -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. +This can be run on every launch without data loss as long as the [migrationCompletedKey] is not altered or deleted. -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 + +```dart +import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; +// ยทยทยท + const SharedPreferencesOptions sharedPreferencesOptions = + SharedPreferencesOptions(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + prefs, sharedPreferencesOptions, 'migrationCompleted'); +``` #### Adding, Removing, or changing prefixes on SharedPreferences diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 003d8d911b5..7756adba4df 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -8,6 +8,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +// #docregion migrate +import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; +// #enddocregion migrate +import 'package:shared_preferences_platform_interface/types.dart'; void main() { runApp(const MyApp()); @@ -61,14 +65,25 @@ class SharedPreferencesDemoState extends State { }); } + Future _migratePreferences() async { + // #docregion migrate + const SharedPreferencesOptions sharedPreferencesOptions = + SharedPreferencesOptions(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + prefs, sharedPreferencesOptions, 'migrationCompleted'); + // #enddocregion migrate + } + @override void initState() { super.initState(); - _counter = _prefs.then((SharedPreferencesWithCache prefs) { - return prefs.getInt('counter') ?? 0; + _migratePreferences().then((_) { + _counter = _prefs.then((SharedPreferencesWithCache prefs) { + return prefs.getInt('counter') ?? 0; + }); + _getExternalCounter(); }); - - _getExternalCounter(); } @override diff --git a/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart index b3d7161014a..566d1e4702d 100644 --- a/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart +++ b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart @@ -10,12 +10,13 @@ import '../shared_preferences.dart'; /// SharedPreferencesAsync. /// /// This method can be run multiple times without worry of overwriting transferred data, -/// as long as the [migrationCompletedKey] is not altered manually. +/// as long as migrationCompletedKey is the same each time, and the value stored +/// under migrationCompletedKey in the target preferences system is not modified. /// /// [legacySharedPreferencesInstance] should be an instance of [SharedPreferences] /// that has been instantiated the same way it has been used throughout your app. -/// If you have called [SharedPreferences.setPrefix] that must be done before using -/// this tool. +/// If you have called [SharedPreferences.setPrefix] that must be done before +/// calling this method. /// /// [sharedPreferencesAsyncOptions] should be an instance of [SharedPreferencesOptions] /// that is set up the way you intend to use the new system going forward. From 91e3b8fb707b1dcfa328a9de19dda524e728cd1d Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 23 Jan 2025 12:15:21 -0800 Subject: [PATCH 4/6] clarifying shampoo --- .../shared_preferences/README.md | 8 +++---- ...ared_preferences_migration_util_test.dart} | 24 +++++++++---------- .../shared_preferences/example/lib/main.dart | 5 +++- .../util/legacy_to_async_migration_util.dart | 18 +++++++------- 4 files changed, 29 insertions(+), 26 deletions(-) rename packages/shared_preferences/shared_preferences/example/integration_test/{shared_preferences_migration_tool_test.dart => shared_preferences_migration_util_test.dart} (90%) diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index 3583618cbeb..638154f1e71 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -161,11 +161,11 @@ await prefsWithCache.clear(); #### Migrating from SharedPreferences to SharedPreferencesAsync/WithCache -Migrating to the newer `SharedPreferencesAsync` or `SharedPreferencesWithCache` API's is straightforward. -All that is need is to import the migration utility, and provide it with the `SharedPreferences` instance -that was being used previously, as well as the options for the desired new API options. +To migrate to the newer `SharedPreferencesAsync` or `SharedPreferencesWithCache` APIs, +import the migration utility and provide it with the `SharedPreferences` instance that +was being used previously, as well as the options for the desired new API options. -This can be run on every launch without data loss as long as the [migrationCompletedKey] is not altered or deleted. +This can be run on every launch without data loss as long as the `migrationCompletedKey` is not altered or deleted. ```dart diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart similarity index 90% rename from packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart rename to packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart index cf67fab8d3b..0aad10f37a5 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_tool_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart @@ -154,9 +154,9 @@ void runTests(SharedPreferencesOptions sharedPreferencesAsyncOptions, testWidgets('data is successfully transferred to new system', (_) async { final SharedPreferences preferences = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: migrationCompletedKey, ); final SharedPreferencesAsync asyncPreferences = @@ -172,9 +172,9 @@ void runTests(SharedPreferencesOptions sharedPreferencesAsyncOptions, testWidgets('migrationCompleted key is set', (_) async { final SharedPreferences preferences = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: migrationCompletedKey, ); final SharedPreferencesAsync asyncPreferences = @@ -189,18 +189,18 @@ void runTests(SharedPreferencesOptions sharedPreferencesAsyncOptions, final SharedPreferences preferences = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: migrationCompletedKey, ); final SharedPreferencesAsync asyncPreferences = SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); await preferences.setInt(intKey, -0); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - preferences, - sharedPreferencesAsyncOptions, - migrationCompletedKey, + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: migrationCompletedKey, ); expect(await asyncPreferences.getInt(intKey), testInt); }, diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 7756adba4df..fd57cf9347b 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -71,7 +71,10 @@ class SharedPreferencesDemoState extends State { SharedPreferencesOptions(); final SharedPreferences prefs = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - prefs, sharedPreferencesOptions, 'migrationCompleted'); + legacySharedPreferencesInstance: prefs, + sharedPreferencesAsyncOptions: sharedPreferencesOptions, + migrationCompletedKey: 'migrationCompleted', + ); // #enddocregion migrate } diff --git a/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart index 566d1e4702d..0fd4cf89b05 100644 --- a/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart +++ b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart @@ -6,12 +6,12 @@ import 'package:shared_preferences_platform_interface/types.dart'; import '../shared_preferences.dart'; -/// Migrates preferences from the legacy SharedPreferences system to -/// SharedPreferencesAsync. +/// Migrates preferences from the legacy [SharedPreferences] system to +/// [SharedPreferencesAsync]. /// /// This method can be run multiple times without worry of overwriting transferred data, -/// as long as migrationCompletedKey is the same each time, and the value stored -/// under migrationCompletedKey in the target preferences system is not modified. +/// as long as [migrationCompletedKey] is the same each time, and the value stored +/// under [migrationCompletedKey] in the target preferences system is not modified. /// /// [legacySharedPreferencesInstance] should be an instance of [SharedPreferences] /// that has been instantiated the same way it has been used throughout your app. @@ -26,11 +26,11 @@ import '../shared_preferences.dart'; /// which is used to check if the migration has run before, to avoid overwriting /// new data going forward. Make sure that there will not be any collisions with /// preferences you are or will be setting going forward, or there may be data loss. -Future migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - SharedPreferences legacySharedPreferencesInstance, - SharedPreferencesOptions sharedPreferencesAsyncOptions, - String migrationCompletedKey, -) async { +Future migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary({ + required SharedPreferences legacySharedPreferencesInstance, + required SharedPreferencesOptions sharedPreferencesAsyncOptions, + required String migrationCompletedKey, +}) async { final SharedPreferencesAsync sharedPreferencesAsyncInstance = SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); From f90c2d9a0840e8dc9791ccf1a3791157dc1ae8b9 Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 23 Jan 2025 12:16:58 -0800 Subject: [PATCH 5/6] reorder changelog --- packages/shared_preferences/shared_preferences/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index bf38c333b94..bec826027c6 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2.4.0 -* Adds clarifying comment about `allowList` handling with an updated prefix. * Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`. +* Adds clarifying comment about `allowList` handling with an updated prefix. ## 2.3.5 From 7407576117947e8b7a7d5ea04282301fe43c392b Mon Sep 17 00:00:00 2001 From: tarrinneal Date: Thu, 23 Jan 2025 13:34:23 -0800 Subject: [PATCH 6/6] snippet --- packages/shared_preferences/shared_preferences/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index 638154f1e71..a1f8a0723bf 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -175,7 +175,10 @@ import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; SharedPreferencesOptions(); final SharedPreferences prefs = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( - prefs, sharedPreferencesOptions, 'migrationCompleted'); + legacySharedPreferencesInstance: prefs, + sharedPreferencesAsyncOptions: sharedPreferencesOptions, + migrationCompletedKey: 'migrationCompleted', + ); ``` #### Adding, Removing, or changing prefixes on SharedPreferences