-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[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
[shared_preferences] Tool for migrating from legacy shared_preferences to shared_preferences_async #8229
Changes from 2 commits
7407d46
dd92917
cd2130e
229944d
9b3e6a5
91e3b8f
f90c2d9
7407576
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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( | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{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), | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
} | ||
|
||
void runAllGroups( | ||
{String? stringValue = testString, bool keysCollide = false}) { | ||
setUp(() async { | ||
await preferences.setBool(boolKey, testBool); | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
}); | ||
}); | ||
}); | ||
} |
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
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 | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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 | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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( | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
SharedPreferences legacySharedPreferencesInstance, | ||
SharedPreferencesOptions sharedPreferencesAsyncOptions, | ||
String migrationCompletedKey, { | ||
bool clearLegacyPreferences = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to not do anything? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The common case will be that people are not setting a prefix (since that was added pretty recently in the |
||
}) async { | ||
final SharedPreferencesAsync sharedPreferencesAsyncInstance = | ||
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions); | ||
|
||
if (await sharedPreferencesAsyncInstance.containsKey(migrationCompletedKey)) { | ||
return; | ||
} | ||
|
||
Set<String> keys = legacySharedPreferencesInstance.getKeys(); | ||
await legacySharedPreferencesInstance.reload(); | ||
keys = legacySharedPreferencesInstance.getKeys(); | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
await sharedPreferencesAsyncInstance.setBool(migrationCompletedKey, true); | ||
|
||
return; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.