Skip to content

Commit 3d28a90

Browse files
authored
[shared_preferences] Tool for migrating from legacy shared_preferences to shared_preferences_async (#8229)
fixes flutter/flutter#150732 fixes flutter/flutter#123153
1 parent 8024c08 commit 3d28a90

File tree

8 files changed

+334
-15
lines changed

8 files changed

+334
-15
lines changed

packages/shared_preferences/shared_preferences/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.4.0
2+
3+
* Adds migration tool to move from legacy `SharedPreferences` to `SharedPreferencesAsync`.
4+
* Adds clarifying comment about `allowList` handling with an updated prefix.
5+
16
## 2.3.5
27

38
* Adds information about Android SharedPreferences support.

packages/shared_preferences/shared_preferences/README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,25 @@ await prefsWithCache.clear();
161161

162162
#### Migrating from SharedPreferences to SharedPreferencesAsync/WithCache
163163

164-
Currently, migration from the older [SharedPreferences] API to the newer
165-
[SharedPreferencesAsync] or [SharedPreferencesWithCache] will need to be done manually.
164+
To migrate to the newer `SharedPreferencesAsync` or `SharedPreferencesWithCache` APIs,
165+
import the migration utility and provide it with the `SharedPreferences` instance that
166+
was being used previously, as well as the options for the desired new API options.
166167

167-
A simple form of this could be fetching all preferences with [SharedPreferences] and adding
168-
them back using [SharedPreferencesAsync], then storing a preference indicating that the
169-
migration has been done so that future runs don't repeat the migration.
168+
This can be run on every launch without data loss as long as the `migrationCompletedKey` is not altered or deleted.
170169

171-
If a migration is not performed before moving to [SharedPreferencesAsync] or [SharedPreferencesWithCache],
172-
most (if not all) data will be lost. Android preferences are stored in a new system, and all platforms
173-
are likely to have some form of enforced prefix (see below) that would not transfer automatically.
174-
175-
A tool to make this process easier can be tracked here: https://github.com/flutter/flutter/issues/150732
170+
<?code-excerpt "main.dart (migrate)"?>
171+
```dart
172+
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';
173+
// ···
174+
const SharedPreferencesOptions sharedPreferencesOptions =
175+
SharedPreferencesOptions();
176+
final SharedPreferences prefs = await SharedPreferences.getInstance();
177+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
178+
legacySharedPreferencesInstance: prefs,
179+
sharedPreferencesAsyncOptions: sharedPreferencesOptions,
180+
migrationCompletedKey: 'migrationCompleted',
181+
);
182+
```
176183

177184
#### Adding, Removing, or changing prefixes on SharedPreferences
178185

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:integration_test/integration_test.dart';
9+
import 'package:shared_preferences/shared_preferences.dart';
10+
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';
11+
import 'package:shared_preferences_android/shared_preferences_android.dart';
12+
import 'package:shared_preferences_foundation/shared_preferences_foundation.dart';
13+
import 'package:shared_preferences_linux/shared_preferences_linux.dart';
14+
import 'package:shared_preferences_platform_interface/types.dart';
15+
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
16+
17+
const String stringKey = 'testString';
18+
const String boolKey = 'testBool';
19+
const String intKey = 'testInt';
20+
const String doubleKey = 'testDouble';
21+
const String listKey = 'testList';
22+
23+
const String testString = 'hello world';
24+
const bool testBool = true;
25+
const int testInt = 42;
26+
const double testDouble = 3.14159;
27+
const List<String> testList = <String>['foo', 'bar'];
28+
29+
const String migrationCompletedKey = 'migrationCompleted';
30+
31+
void main() {
32+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
33+
34+
group('SharedPreferences without setting prefix', () {
35+
runAllGroups(() {});
36+
});
37+
38+
group('SharedPreferences with setPrefix', () {
39+
runAllGroups(() {
40+
SharedPreferences.setPrefix('prefix.');
41+
});
42+
});
43+
44+
group('SharedPreferences with setPrefix and allowList', () {
45+
runAllGroups(
46+
() {
47+
final Set<String> allowList = <String>{
48+
'prefix.$boolKey',
49+
'prefix.$intKey',
50+
'prefix.$doubleKey',
51+
'prefix.$listKey'
52+
};
53+
SharedPreferences.setPrefix('prefix.', allowList: allowList);
54+
},
55+
stringValue: null,
56+
);
57+
});
58+
59+
group('SharedPreferences with prefix set to empty string', () {
60+
runAllGroups(
61+
() {
62+
SharedPreferences.setPrefix('');
63+
},
64+
keysCollide: true,
65+
);
66+
});
67+
}
68+
69+
void runAllGroups(void Function() legacySharedPrefsConfig,
70+
{String? stringValue = testString, bool keysCollide = false}) {
71+
group('default sharedPreferencesAsyncOptions', () {
72+
const SharedPreferencesOptions sharedPreferencesAsyncOptions =
73+
SharedPreferencesOptions();
74+
75+
runTests(
76+
sharedPreferencesAsyncOptions,
77+
legacySharedPrefsConfig,
78+
stringValue: stringValue,
79+
keysAndNamesCollide: keysCollide,
80+
);
81+
});
82+
83+
group('file name (or equivalent) sharedPreferencesAsyncOptions', () {
84+
final SharedPreferencesOptions sharedPreferencesAsyncOptions;
85+
if (Platform.isAndroid) {
86+
sharedPreferencesAsyncOptions =
87+
const SharedPreferencesAsyncAndroidOptions(
88+
backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences,
89+
originalSharedPreferencesOptions: AndroidSharedPreferencesStoreOptions(
90+
fileName: 'fileName',
91+
),
92+
);
93+
} else if (Platform.isIOS || Platform.isMacOS) {
94+
sharedPreferencesAsyncOptions =
95+
SharedPreferencesAsyncFoundationOptions(suiteName: 'group.fileName');
96+
} else if (Platform.isLinux) {
97+
sharedPreferencesAsyncOptions = const SharedPreferencesLinuxOptions(
98+
fileName: 'fileName',
99+
);
100+
} else if (Platform.isWindows) {
101+
sharedPreferencesAsyncOptions =
102+
const SharedPreferencesWindowsOptions(fileName: 'fileName');
103+
} else {
104+
sharedPreferencesAsyncOptions = const SharedPreferencesOptions();
105+
}
106+
107+
runTests(
108+
sharedPreferencesAsyncOptions,
109+
legacySharedPrefsConfig,
110+
stringValue: stringValue,
111+
);
112+
});
113+
114+
if (Platform.isAndroid) {
115+
group('Android default sharedPreferences', () {
116+
const SharedPreferencesOptions sharedPreferencesAsyncOptions =
117+
SharedPreferencesAsyncAndroidOptions(
118+
backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences,
119+
originalSharedPreferencesOptions:
120+
AndroidSharedPreferencesStoreOptions(),
121+
);
122+
123+
runTests(
124+
sharedPreferencesAsyncOptions,
125+
legacySharedPrefsConfig,
126+
stringValue: stringValue,
127+
);
128+
});
129+
}
130+
}
131+
132+
void runTests(SharedPreferencesOptions sharedPreferencesAsyncOptions,
133+
void Function() legacySharedPrefsConfig,
134+
{String? stringValue = testString, bool keysAndNamesCollide = false}) {
135+
setUp(() async {
136+
// Configure and populate the source legacy shared preferences.
137+
SharedPreferences.resetStatic();
138+
legacySharedPrefsConfig();
139+
140+
final SharedPreferences preferences = await SharedPreferences.getInstance();
141+
await preferences.clear();
142+
await preferences.setBool(boolKey, testBool);
143+
await preferences.setInt(intKey, testInt);
144+
await preferences.setDouble(doubleKey, testDouble);
145+
await preferences.setString(stringKey, testString);
146+
await preferences.setStringList(listKey, testList);
147+
});
148+
149+
tearDown(() async {
150+
await SharedPreferencesAsync(options: sharedPreferencesAsyncOptions)
151+
.clear();
152+
});
153+
154+
testWidgets('data is successfully transferred to new system', (_) async {
155+
final SharedPreferences preferences = await SharedPreferences.getInstance();
156+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
157+
legacySharedPreferencesInstance: preferences,
158+
sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions,
159+
migrationCompletedKey: migrationCompletedKey,
160+
);
161+
162+
final SharedPreferencesAsync asyncPreferences =
163+
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions);
164+
165+
expect(await asyncPreferences.getBool(boolKey), testBool);
166+
expect(await asyncPreferences.getInt(intKey), testInt);
167+
expect(await asyncPreferences.getDouble(doubleKey), testDouble);
168+
expect(await asyncPreferences.getString(stringKey), stringValue);
169+
expect(await asyncPreferences.getStringList(listKey), testList);
170+
});
171+
172+
testWidgets('migrationCompleted key is set', (_) async {
173+
final SharedPreferences preferences = await SharedPreferences.getInstance();
174+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
175+
legacySharedPreferencesInstance: preferences,
176+
sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions,
177+
migrationCompletedKey: migrationCompletedKey,
178+
);
179+
180+
final SharedPreferencesAsync asyncPreferences =
181+
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions);
182+
183+
expect(await asyncPreferences.getBool(migrationCompletedKey), true);
184+
});
185+
186+
testWidgets(
187+
're-running migration tool does not overwrite data',
188+
(_) async {
189+
final SharedPreferences preferences =
190+
await SharedPreferences.getInstance();
191+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
192+
legacySharedPreferencesInstance: preferences,
193+
sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions,
194+
migrationCompletedKey: migrationCompletedKey,
195+
);
196+
197+
final SharedPreferencesAsync asyncPreferences =
198+
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions);
199+
await preferences.setInt(intKey, -0);
200+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
201+
legacySharedPreferencesInstance: preferences,
202+
sharedPreferencesAsyncOptions: sharedPreferencesAsyncOptions,
203+
migrationCompletedKey: migrationCompletedKey,
204+
);
205+
expect(await asyncPreferences.getInt(intKey), testInt);
206+
},
207+
// Skips platforms that would be adding the preferences to the same file.
208+
skip: keysAndNamesCollide &&
209+
(Platform.isWindows ||
210+
Platform.isLinux ||
211+
Platform.isMacOS ||
212+
Platform.isIOS),
213+
);
214+
}

packages/shared_preferences/shared_preferences/example/lib/main.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import 'dart:async';
88

99
import 'package:flutter/material.dart';
1010
import 'package:shared_preferences/shared_preferences.dart';
11+
// #docregion migrate
12+
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';
13+
// #enddocregion migrate
14+
import 'package:shared_preferences_platform_interface/types.dart';
1115

1216
void main() {
1317
runApp(const MyApp());
@@ -61,14 +65,28 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
6165
});
6266
}
6367

68+
Future<void> _migratePreferences() async {
69+
// #docregion migrate
70+
const SharedPreferencesOptions sharedPreferencesOptions =
71+
SharedPreferencesOptions();
72+
final SharedPreferences prefs = await SharedPreferences.getInstance();
73+
await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
74+
legacySharedPreferencesInstance: prefs,
75+
sharedPreferencesAsyncOptions: sharedPreferencesOptions,
76+
migrationCompletedKey: 'migrationCompleted',
77+
);
78+
// #enddocregion migrate
79+
}
80+
6481
@override
6582
void initState() {
6683
super.initState();
67-
_counter = _prefs.then((SharedPreferencesWithCache prefs) {
68-
return prefs.getInt('counter') ?? 0;
84+
_migratePreferences().then((_) {
85+
_counter = _prefs.then((SharedPreferencesWithCache prefs) {
86+
return prefs.getInt('counter') ?? 0;
87+
});
88+
_getExternalCounter();
6989
});
70-
71-
_getExternalCounter();
7290
}
7391

7492
@override

packages/shared_preferences/shared_preferences/example/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ dependencies:
1717
# the parent directory to use the current plugin's version.
1818
path: ../
1919
shared_preferences_android: ^2.4.0
20+
shared_preferences_foundation: ^2.5.3
21+
shared_preferences_linux: ^2.4.1
2022
shared_preferences_platform_interface: ^2.4.0
23+
shared_preferences_windows: ^2.4.1
2124

2225
dev_dependencies:
2326
build_runner: ^2.1.10

packages/shared_preferences/shared_preferences/lib/src/shared_preferences_legacy.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class SharedPreferences {
4444
/// [allowList] will cause the plugin to only return preferences that
4545
/// are both contained in the list AND match the provided prefix.
4646
///
47+
/// If [prefix] is changed, and an [allowList] is used, the prefix must be included
48+
/// on the keys added to the [allowList].
49+
///
4750
/// No migration of existing preferences is performed by this method.
4851
/// If you set a different prefix, and have previously stored preferences,
4952
/// you will need to handle any migration yourself.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:shared_preferences_platform_interface/types.dart';
6+
7+
import '../shared_preferences.dart';
8+
9+
/// Migrates preferences from the legacy [SharedPreferences] system to
10+
/// [SharedPreferencesAsync].
11+
///
12+
/// This method can be run multiple times without worry of overwriting transferred data,
13+
/// as long as [migrationCompletedKey] is the same each time, and the value stored
14+
/// under [migrationCompletedKey] in the target preferences system is not modified.
15+
///
16+
/// [legacySharedPreferencesInstance] should be an instance of [SharedPreferences]
17+
/// that has been instantiated the same way it has been used throughout your app.
18+
/// If you have called [SharedPreferences.setPrefix] that must be done before
19+
/// calling this method.
20+
///
21+
/// [sharedPreferencesAsyncOptions] should be an instance of [SharedPreferencesOptions]
22+
/// that is set up the way you intend to use the new system going forward.
23+
/// This tool will allow for future use of [SharedPreferencesAsync] and [SharedPreferencesWithCache].
24+
///
25+
/// The [migrationCompletedKey] is a key that is stored in the target preferences
26+
/// which is used to check if the migration has run before, to avoid overwriting
27+
/// new data going forward. Make sure that there will not be any collisions with
28+
/// preferences you are or will be setting going forward, or there may be data loss.
29+
Future<void> migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary({
30+
required SharedPreferences legacySharedPreferencesInstance,
31+
required SharedPreferencesOptions sharedPreferencesAsyncOptions,
32+
required String migrationCompletedKey,
33+
}) async {
34+
final SharedPreferencesAsync sharedPreferencesAsyncInstance =
35+
SharedPreferencesAsync(options: sharedPreferencesAsyncOptions);
36+
37+
if (await sharedPreferencesAsyncInstance.containsKey(migrationCompletedKey)) {
38+
return;
39+
}
40+
41+
await legacySharedPreferencesInstance.reload();
42+
final Set<String> keys = legacySharedPreferencesInstance.getKeys();
43+
44+
for (final String key in keys) {
45+
final Object? value = legacySharedPreferencesInstance.get(key);
46+
switch (value.runtimeType) {
47+
case const (bool):
48+
await sharedPreferencesAsyncInstance.setBool(key, value! as bool);
49+
case const (int):
50+
await sharedPreferencesAsyncInstance.setInt(key, value! as int);
51+
case const (double):
52+
await sharedPreferencesAsyncInstance.setDouble(key, value! as double);
53+
case const (String):
54+
await sharedPreferencesAsyncInstance.setString(key, value! as String);
55+
case const (List<String>):
56+
case const (List<String?>):
57+
case const (List<Object?>):
58+
case const (List<dynamic>):
59+
try {
60+
await sharedPreferencesAsyncInstance.setStringList(
61+
key, (value! as List<Object?>).cast<String>());
62+
} on TypeError catch (_) {} // Pass over Lists containing non-String values.
63+
}
64+
}
65+
66+
await sharedPreferencesAsyncInstance.setBool(migrationCompletedKey, true);
67+
68+
return;
69+
}

0 commit comments

Comments
 (0)