diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 7dfad33da36..bec826027c6 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.4.0 + +* Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`. +* Adds clarifying comment about `allowList` handling with an updated prefix. + ## 2.3.5 * Adds information about Android SharedPreferences support. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index ad6c7268eed..a1f8a0723bf 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -161,18 +161,25 @@ 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. +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. -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( + legacySharedPreferencesInstance: prefs, + sharedPreferencesAsyncOptions: sharedPreferencesOptions, + migrationCompletedKey: 'migrationCompleted', + ); +``` #### Adding, Removing, or changing prefixes on SharedPreferences diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart new file mode 100644 index 00000000000..0aad10f37a5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_migration_util_test.dart @@ -0,0 +1,214 @@ +// 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/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'; + +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']; + +const String migrationCompletedKey = 'migrationCompleted'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferences without setting prefix', () { + runAllGroups(() {}); + }); + + group('SharedPreferences with setPrefix', () { + runAllGroups(() { + SharedPreferences.setPrefix('prefix.'); + }); + }); + + group('SharedPreferences with setPrefix and allowList', () { + runAllGroups( + () { + final Set allowList = { + 'prefix.$boolKey', + 'prefix.$intKey', + 'prefix.$doubleKey', + 'prefix.$listKey' + }; + SharedPreferences.setPrefix('prefix.', allowList: allowList); + }, + stringValue: null, + ); + }); + + group('SharedPreferences with prefix set to empty string', () { + runAllGroups( + () { + SharedPreferences.setPrefix(''); + }, + 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( + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: 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( + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: 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( + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: migrationCompletedKey, + ); + + final SharedPreferencesAsync asyncPreferences = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + await preferences.setInt(intKey, -0); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + legacySharedPreferencesInstance: preferences, + sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions, + migrationCompletedKey: 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/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 003d8d911b5..fd57cf9347b 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,28 @@ class SharedPreferencesDemoState extends State { }); } + Future _migratePreferences() async { + // #docregion migrate + const SharedPreferencesOptions sharedPreferencesOptions = + SharedPreferencesOptions(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( + legacySharedPreferencesInstance: prefs, + sharedPreferencesAsyncOptions: sharedPreferencesOptions, + migrationCompletedKey: '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/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 96753eed795..5956b5ba0c2 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -17,7 +17,10 @@ dependencies: # 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/util/legacy_to_async_migration_util.dart b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart new file mode 100644 index 00000000000..0fd4cf89b05 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/lib/util/legacy_to_async_migration_util.dart @@ -0,0 +1,69 @@ +// 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'; + +/// 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. +/// +/// [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 +/// 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. +/// This tool will allow for future use of [SharedPreferencesAsync] and [SharedPreferencesWithCache]. +/// +/// 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({ + required SharedPreferences legacySharedPreferencesInstance, + required SharedPreferencesOptions sharedPreferencesAsyncOptions, + required String migrationCompletedKey, +}) async { + final SharedPreferencesAsync sharedPreferencesAsyncInstance = + SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); + + if (await sharedPreferencesAsyncInstance.containsKey(migrationCompletedKey)) { + return; + } + + await legacySharedPreferencesInstance.reload(); + final Set 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()); + } on TypeError 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 d819a46785f..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.5 +version: 2.4.0 environment: sdk: ^3.5.0