Skip to content

[shared_preferences] Tool for migrating from legacy shared_preferences to shared_preferences_async #8229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/shared_preferences/shared_preferences/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.3.6

* Adds clarifying comment about allowList handling with an updated prefix.
* Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`.

## 2.3.5

* Adds information about Android SharedPreferences support.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> testList = <String>['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<String> allowList = <String>{
'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);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> migrateLegacySharedPreferencesToSharedPreferencesAsync(
SharedPreferences legacySharedPreferencesInstance,
SharedPreferencesOptions sharedPreferencesAsyncOptions,
String migrationCompletedKey, {
bool clearLegacyPreferences = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to not do anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot about that. Is this a feature that's worth adding? I hadn't yet as it seemed like it could be problematic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could wait and see if anyone asks. On one hand, I would generally expect a migration to move data instead of duplicating it. On the other hand, it shouldn't be a lot of data.

My one real hesitation here is that if someone isn't using an allow list, on most platforms this would mean that an unprefixed new-style prefs object would be loading all the old prefs too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also means that items not set by our plugin would get deleted. Potentially if not used carefully (or written carefully) it could write to the same file, then delete the prefs it just wrote.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also means that items not set by our plugin would get deleted.

The common case will be that people are not setting a prefix (since that was added pretty recently in the shared_preferences lifetime, and is opt-in even once available). In that scenario, clearing legacy prefs can't delete anything we didn't write.

}) async {
final SharedPreferencesAsync sharedPreferencesAsyncInstance =
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions);

if (await sharedPreferencesAsyncInstance.containsKey(migrationCompletedKey)) {
return;
}

Set<String> 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<String>):
case const (List<String?>):
case const (List<Object?>):
case const (List<dynamic>):
try {
await sharedPreferencesAsyncInstance.setStringList(
key, (value! as List<Object?>).cast<String>());
} catch (_) {} // Pass over Lists containing non-String values.
}
}

await sharedPreferencesAsyncInstance.setBool(migrationCompletedKey, true);

return;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.3.6

environment:
sdk: ^3.5.0
Expand Down
Loading