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 all 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.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
Loading