Skip to content

Commit

Permalink
[shared_preferences] Tool for migrating from legacy shared_preference…
Browse files Browse the repository at this point in the history
…s to shared_preferences_async (#8229)

fixes flutter/flutter#150732
fixes flutter/flutter#123153
  • Loading branch information
tarrinneal authored Jan 24, 2025
1 parent 8024c08 commit 3d28a90
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 15 deletions.
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.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.
Expand Down
27 changes: 17 additions & 10 deletions packages/shared_preferences/shared_preferences/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?code-excerpt "main.dart (migrate)"?>
```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

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> testList = <String>['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<String> allowList = <String>{
'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),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -61,14 +65,28 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
});
}

Future<void> _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
Expand Down
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,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<void> 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<String> 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>());
} on TypeError catch (_) {} // Pass over Lists containing non-String values.
}
}

await sharedPreferencesAsyncInstance.setBool(migrationCompletedKey, true);

return;
}
Loading

0 comments on commit 3d28a90

Please sign in to comment.