Skip to content

Commit

Permalink
[shared_preferences] allow custom key prefixes (flutter#3465)
Browse files Browse the repository at this point in the history
[shared_preferences] allow custom key prefixes
  • Loading branch information
tarrinneal authored Mar 31, 2023
1 parent e4dabc0 commit 6502403
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 31 deletions.
4 changes: 4 additions & 0 deletions packages/shared_preferences/shared_preferences/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.1.0

* Adds `setPrefix` method.

## 2.0.20

* Adds README discussion of `reload()`.
Expand Down
24 changes: 24 additions & 0 deletions packages/shared_preferences/shared_preferences/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@ If this is problematic for your use case, you can thumbs up
interest in APIs that provide direct (asynchronous) access to the underlying
preference store, and/or subscribe to it for updates.

### Migration and Prefixes

By default, the `SharedPreferences` plugin will only read (and write) preferences
that begin with the prefix `flutter.`. This is all handled internally by the plugin
and does not require manually adding this prefix.

Alternatively, `SharedPreferences` can be configured to use any prefix by adding
a call to `setPrefix` before any instances of `SharedPreferences` are instantiated.
Calling `setPrefix` after an instance of `SharedPreferences` is created will fail.
Setting the prefix to an empty string `''` will allow access to all preferences created
by any non-flutter versions of the app (for migrating from a native app to flutter).

If the prefix is set to a value such as `''` that causes it to read values that were
not originally stored by the `SharedPreferences`, initializing `SharedPreferences`
may fail if any of the values are of types that are not supported by `SharedPreferences`.

If you decide to remove the prefix entirely, you can still access previously created
preferences by manually adding the previous prefix `flutter.` to the beginning of
the preference key.

If you have been using `SharedPreferences` with the default prefix but wish to change
to a new prefix, you will need to transform your current preferences manually to add
the new prefix otherwise the old preferences will be inaccessible.

### Testing

In tests, you can replace the standard `SharedPreferences` implementation with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,21 @@ import 'package:shared_preferences/shared_preferences.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('$SharedPreferences', () {
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 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 testString2 = 'goodbye world';
const bool testBool2 = false;
const int testInt2 = 1337;
const double testDouble2 = 2.71828;
const List<String> testList2 = <String>['baz', 'quox'];
const String testString2 = 'goodbye world';
const bool testBool2 = false;
const int testInt2 = 1337;
const double testDouble2 = 2.71828;
const List<String> testList2 = <String>['baz', 'quox'];

late SharedPreferences preferences;

setUp(() async {
preferences = await SharedPreferences.getInstance();
});

tearDown(() {
preferences.clear();
});
late SharedPreferences preferences;

void runAllTests() {
testWidgets('reading', (WidgetTester _) async {
expect(preferences.get('String'), isNull);
expect(preferences.get('bool'), isNull);
Expand Down Expand Up @@ -97,5 +89,48 @@ void main() {
// The last write should win.
expect(preferences.getInt('int'), writeCount);
});
}

group('SharedPreferences', () {
setUp(() async {
preferences = await SharedPreferences.getInstance();
});

tearDown(() {
preferences.clear();
SharedPreferences.resetStatic();
});

runAllTests();
});

group('setPrefix', () {
setUp(() async {
SharedPreferences.resetStatic();
SharedPreferences.setPrefix('prefix.');
preferences = await SharedPreferences.getInstance();
});

tearDown(() {
preferences.clear();
SharedPreferences.resetStatic();
});

runAllTests();
});

group('setNoPrefix', () {
setUp(() async {
SharedPreferences.resetStatic();
SharedPreferences.setPrefix('');
preferences = await SharedPreferences.getInstance();
});

tearDown(() {
preferences.clear();
SharedPreferences.resetStatic();
});

runAllTests();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,46 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor
class SharedPreferences {
SharedPreferences._(this._preferenceCache);

static const String _prefix = 'flutter.';
static String _prefix = 'flutter.';

static bool _prefixHasBeenChanged = false;

static Completer<SharedPreferences>? _completer;

static SharedPreferencesStorePlatform get _store =>
SharedPreferencesStorePlatform.instance;

/// Sets the prefix that is attached to all keys for all shared preferences.
///
/// This changes the inputs when adding data to preferences as well as
/// setting the filter that determines what data will be returned
/// from the `getInstance` method.
///
/// By default, the prefix is 'flutter.', which is compatible with the
/// previous behavior of this plugin. To use preferences with no prefix,
/// set [prefix] to ''.
///
/// 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.
///
/// This cannot be called after `getInstance`.
static void setPrefix(String prefix) {
if (_completer != null) {
throw StateError('setPrefix cannot be called after getInstance');
}
_prefix = prefix;
_prefixHasBeenChanged = true;
}

/// Resets class's static values to allow for testing of setPrefix flow.
@visibleForTesting
static void resetStatic() {
_completer = null;
_prefix = 'flutter.';
_prefixHasBeenChanged = false;
}

/// Loads and parses the [SharedPreferences] for this app from disk.
///
/// Because this is reading from disk, it shouldn't be awaited in
Expand Down Expand Up @@ -146,6 +180,21 @@ class SharedPreferences {
/// Completes with true once the user preferences for the app has been cleared.
Future<bool> clear() {
_preferenceCache.clear();
if (_prefixHasBeenChanged) {
try {
return _store.clearWithPrefix(_prefix);
} catch (e) {
// Catching and clarifying UnimplementedError to provide a more robust message.
if (e is UnimplementedError) {
throw UnimplementedError('''
This implementation of Shared Preferences doesn't yet support the setPrefix method.
Either update the implementation to support setPrefix, or do not call setPrefix.
''');
} else {
rethrow;
}
}
}
return _store.clear();
}

Expand All @@ -161,9 +210,29 @@ class SharedPreferences {
}

static Future<Map<String, Object>> _getSharedPreferencesMap() async {
final Map<String, Object> fromSystem = await _store.getAll();
assert(fromSystem != null);
// Strip the flutter. prefix from the returned preferences.
final Map<String, Object> fromSystem = <String, Object>{};
if (_prefixHasBeenChanged) {
try {
fromSystem.addAll(await _store.getAllWithPrefix(_prefix));
} catch (e) {
// Catching and clarifying UnimplementedError to provide a more robust message.
if (e is UnimplementedError) {
throw UnimplementedError('''
This implementation of Shared Preferences doesn't yet support the setPrefix method.
Either update the implementation to support setPrefix, or do not call setPrefix.
''');
} else {
rethrow;
}
}
} else {
fromSystem.addAll(await _store.getAll());
}

if (_prefix.isEmpty) {
return fromSystem;
}
// Strip the prefix from the returned preferences.
final Map<String, Object> preferencesMap = <String, Object>{};
for (final String key in fromSystem.keys) {
assert(key.startsWith(_prefix));
Expand Down
14 changes: 7 additions & 7 deletions packages/shared_preferences/shared_preferences/pubspec.yaml
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.0.20
version: 2.1.0

environment:
sdk: ">=2.17.0 <4.0.0"
Expand All @@ -28,12 +28,12 @@ flutter:
dependencies:
flutter:
sdk: flutter
shared_preferences_android: ^2.0.8
shared_preferences_foundation: ^2.1.0
shared_preferences_linux: ^2.0.1
shared_preferences_platform_interface: ^2.0.0
shared_preferences_web: ^2.0.0
shared_preferences_windows: ^2.0.1
shared_preferences_android: ^2.1.0
shared_preferences_foundation: ^2.2.0
shared_preferences_linux: ^2.2.0
shared_preferences_platform_interface: ^2.2.0
shared_preferences_web: ^2.1.0
shared_preferences_windows: ^2.2.0

dev_dependencies:
flutter_driver:
Expand Down
Loading

0 comments on commit 6502403

Please sign in to comment.