Repository navigation
SharedPreferences should have better singleton instantiation #42407
Copy link
Copy link
Closed
flutter/packages
#4159Closed
Copy link
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listp: shared_preferencesPlugin to read and write Shared PreferencesPlugin to read and write Shared Preferencespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.
Description
As discussed in this StackOverflow question, the getInstance() method in the SharedPreferences plugin is not reliable when called from both a microtask and regular code. The problem seems to be that _instance == null will be true until await _getSharedPreferencesMap(); has returned, and therefor it's possible to get two different instances from this singleton factory.
static Future<SharedPreferences> getInstance() async {
if (_instance == null) {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
_instance = SharedPreferences._(preferencesMap);
}
return _instance;
}
Steps to Reproduce
@override
void initState() {
initAsync();
super.initState();
}
initAsync() async {
scheduleMicrotask(() async {
print("microtask: START ");
TestClass microtask = await TestClass.getInstance("microtask");
print("microtask: FINISH (instance: ${microtask.hashCode})");
});
print("initAsync_1: START");
TestClass initAsync_1 = await TestClass.getInstance("initAsync_1");
print("initAsync_1: FINISH (instance: ${initAsync_1.hashCode})");
Future.delayed(Duration(seconds: 3)).then((_) async {
print("initAsync_2: START");
TestClass initAsync_2 = await TestClass.getInstance("initAsync_2");
print("initAsync_2: FINISH (instance: ${initAsync_2.hashCode})");
});
}
class TestClass {
static TestClass _instance;
static Future<TestClass> getInstance(String call) async {
if (_instance == null) {
print("$call: without instance before");
await Future.delayed(Duration(seconds: 3));
_instance = TestClass();
print("$call: without instance after");
} else {
print("$call: with instance");
}
return _instance;
}
}
Logs
initAsync_1: START
initAsync_1: without instance before
microtask: START
microtask: without instance before
initAsync_1: without instance after
initAsync_1: FINISH (instance: 674207757)
microtask: without instance after
microtask: FINISH (instance: 1059788553)
initAsync_2: START
initAsync_2: with instance
initAsync_2: FINISH (instance: 1059788553)
Possible solution
One possible solution would be to use a Completer:
static Future<SharedPreferences> getInstance() async {
if( _initialization != null && !_initialization.isCompleted ) {
await _initialization.future;
}
if (_instance == null) {
_initialization = Completer<void>();
try {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
_instance = SharedPreferences._(preferencesMap);
}
finally{
_initialization.complete();
}
}
return _instance;
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listp: shared_preferencesPlugin to read and write Shared PreferencesPlugin to read and write Shared Preferencespackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.
Activity
sanekyy commentedon May 19, 2022
This issue still actual and can produce more than 1 instance:(
gslender commentedon Jun 5, 2023
I'm seeing this pop up on lots of iOS users using a production app. Been tearing out my hair trying to figure this one out and why we're seeing issues in production that are hard to replicate in test.
Is there any workaround that can be applied to using SharedPreferences to avoid this bug?
gslender commentedon Jun 5, 2023
By "this" I mean cases where data stored is not able to be read moments later in the app. There is definitely some challenges around the async nature and even using reload() seems to have no improvement in ensuring the data is kept. I'm always using await with setString but for some unexplained reason, there are cases when getString followed after a setString results in the value not being stored !?
gslender commentedon Jun 7, 2023
I've raised an issue #128368 that speaks to the issue I'm having, of which this issue here seems to be related
stuartmorgan-g commentedon Jun 7, 2023
It turns out that this was fixed years ago (slightly before this issue was filed, in fact), but regressed during the NNBD transition.
stuartmorgan-g commentedon Jun 7, 2023
If calling
reload()before reading doesn't help, then your case is unrelated to this one. This could cause there to be different instances of the Dart-side cache, but the purpose ofreload()is to update that cache with the current native state.[shared_preferences] Fix initialization race
[shared_preferences] Fix initialization race (#4159)
github-actions commentedon Jun 22, 2023
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.[shared_preferences] Fix initialization race (flutter#4159)
[shared_preferences] Fix initialization race (flutter#4159)