blob: 6164f1bb242317c05fd6dca1696d0f66b0f59d89 [file] [log] [blame]
// 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:async';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';
/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing
/// a persistent store for simple data.
///
/// Data is persisted to disk asynchronously.
class SharedPreferences {
SharedPreferences._(this._preferenceCache);
static String _prefix = 'flutter.';
static bool _prefixHasBeenChanged = false;
static Set<String>? _allowList;
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 ''.
///
/// If [prefix] is set to '', you may encounter preferences that are
/// incompatible with shared_preferences. The optional parameter
/// [allowList] will cause the plugin to only return preferences that
/// are both contained in the list AND match the provided prefix.
///
/// 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, {Set<String>? allowList}) {
if (_completer != null) {
throw StateError('setPrefix cannot be called after getInstance');
}
_prefix = prefix;
_prefixHasBeenChanged = true;
_allowList = allowList;
}
/// Resets class's static values to allow for testing of setPrefix flow.
@visibleForTesting
static void resetStatic() {
_completer = null;
_prefix = 'flutter.';
_prefixHasBeenChanged = false;
_allowList = null;
}
/// Loads and parses the [SharedPreferences] for this app from disk.
///
/// Because this is reading from disk, it shouldn't be awaited in
/// performance-sensitive blocks.
static Future<SharedPreferences> getInstance() async {
if (_completer == null) {
final Completer<SharedPreferences> completer =
Completer<SharedPreferences>();
_completer = completer;
try {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
completer.complete(SharedPreferences._(preferencesMap));
} catch (e) {
// If there's an error, explicitly return the future with an error.
// then set the completer to null so we can retry.
completer.completeError(e);
final Future<SharedPreferences> sharedPrefsFuture = completer.future;
_completer = null;
return sharedPrefsFuture;
}
}
return _completer!.future;
}
/// The cache that holds all preferences.
///
/// It is instantiated to the current state of the SharedPreferences or
/// NSUserDefaults object and then kept in sync via setter methods in this
/// class.
///
/// It is NOT guaranteed that this cache and the device prefs will remain
/// in sync since the setter method might fail for any reason.
final Map<String, Object> _preferenceCache;
/// Returns all keys in the persistent storage.
Set<String> getKeys() => Set<String>.from(_preferenceCache.keys);
/// Reads a value of any type from persistent storage.
Object? get(String key) => _preferenceCache[key];
/// Reads a value from persistent storage, throwing an exception if it's not a
/// bool.
bool? getBool(String key) => _preferenceCache[key] as bool?;
/// Reads a value from persistent storage, throwing an exception if it's not
/// an int.
int? getInt(String key) => _preferenceCache[key] as int?;
/// Reads a value from persistent storage, throwing an exception if it's not a
/// double.
double? getDouble(String key) => _preferenceCache[key] as double?;
/// Reads a value from persistent storage, throwing an exception if it's not a
/// String.
String? getString(String key) => _preferenceCache[key] as String?;
/// Returns true if persistent storage the contains the given [key].
bool containsKey(String key) => _preferenceCache.containsKey(key);
/// Reads a set of string values from persistent storage, throwing an
/// exception if it's not a string set.
List<String>? getStringList(String key) {
List<dynamic>? list = _preferenceCache[key] as List<dynamic>?;
if (list != null && list is! List<String>) {
list = list.cast<String>().toList();
_preferenceCache[key] = list;
}
// Make a copy of the list so that later mutations won't propagate
return list?.toList() as List<String>?;
}
/// Saves a boolean [value] to persistent storage in the background.
Future<bool> setBool(String key, bool value) => _setValue('Bool', key, value);
/// Saves an integer [value] to persistent storage in the background.
Future<bool> setInt(String key, int value) => _setValue('Int', key, value);
/// Saves a double [value] to persistent storage in the background.
///
/// Android doesn't support storing doubles, so it will be stored as a float.
Future<bool> setDouble(String key, double value) =>
_setValue('Double', key, value);
/// Saves a string [value] to persistent storage in the background.
///
/// Note: Due to limitations in Android's SharedPreferences,
/// values cannot start with any one of the following:
///
/// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu'
/// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy'
/// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu'
Future<bool> setString(String key, String value) =>
_setValue('String', key, value);
/// Saves a list of strings [value] to persistent storage in the background.
Future<bool> setStringList(String key, List<String> value) =>
_setValue('StringList', key, value);
/// Removes an entry from persistent storage.
Future<bool> remove(String key) {
final String prefixedKey = '$_prefix$key';
_preferenceCache.remove(key);
return _store.remove(prefixedKey);
}
Future<bool> _setValue(String valueType, String key, Object value) {
ArgumentError.checkNotNull(value, 'value');
final String prefixedKey = '$_prefix$key';
if (value is List<String>) {
// Make a copy of the list so that later mutations won't propagate
_preferenceCache[key] = value.toList();
} else {
_preferenceCache[key] = value;
}
return _store.setValue(valueType, prefixedKey, value);
}
/// Always returns true.
/// On iOS, synchronize is marked deprecated. On Android, we commit every set.
@Deprecated('This method is now a no-op, and should no longer be called.')
Future<bool> commit() async => true;
/// Completes with true once the user preferences for the app has been cleared.
Future<bool> clear() {
_preferenceCache.clear();
if (_prefixHasBeenChanged) {
try {
return _store.clearWithParameters(
ClearParameters(
filter: PreferencesFilter(
prefix: _prefix,
allowList: _allowList,
),
),
);
} 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();
}
/// Fetches the latest values from the host platform.
///
/// Use this method to observe modifications that were made in native code
/// (without using the plugin) while the app is running.
Future<void> reload() async {
final Map<String, Object> preferences =
await SharedPreferences._getSharedPreferencesMap();
_preferenceCache.clear();
_preferenceCache.addAll(preferences);
}
static Future<Map<String, Object>> _getSharedPreferencesMap() async {
final Map<String, Object> fromSystem = <String, Object>{};
if (_prefixHasBeenChanged) {
try {
fromSystem.addAll(
await _store.getAllWithParameters(
GetAllParameters(
filter: PreferencesFilter(
prefix: _prefix,
allowList: _allowList,
),
),
),
);
} 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));
preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!;
}
return preferencesMap;
}
/// Initializes the shared preferences with mock values for testing.
///
/// If the singleton instance has been initialized already, it is nullified.
@visibleForTesting
static void setMockInitialValues(Map<String, Object> values) {
final Map<String, Object> newValues =
values.map<String, Object>((String key, Object value) {
String newKey = key;
if (!key.startsWith(_prefix)) {
newKey = '$_prefix$key';
}
return MapEntry<String, Object>(newKey, value);
});
SharedPreferencesStorePlatform.instance =
InMemorySharedPreferencesStore.withData(newValues);
_completer = null;
}
}