[shared_preferences] Fix initialization race (#4159)

During the NNBD transition, the structure of the completer logic in the initialization flow was incorrectly changed to set the field after an `await` instead of immediately.

Also updates the error handling to handle `Error` the same way it currently handles `Exception`, which this change surfaced.

Fixes https://github.com/flutter/flutter/issues/42407
This commit is contained in:
stuartmorgan
2023-06-07 22:34:57 -04:00
committed by GitHub
parent 010ba50128
commit d935cb0d2f
4 changed files with 196 additions and 178 deletions

View File

@ -1,5 +1,7 @@
## NEXT ## 2.1.2
* Fixes singleton initialization race condition introduced during NNBD
transition.
* Updates minimum supported macOS version to 10.14. * Updates minimum supported macOS version to 10.14.
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.

View File

@ -62,11 +62,12 @@ class SharedPreferences {
if (_completer == null) { if (_completer == null) {
final Completer<SharedPreferences> completer = final Completer<SharedPreferences> completer =
Completer<SharedPreferences>(); Completer<SharedPreferences>();
_completer = completer;
try { try {
final Map<String, Object> preferencesMap = final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap(); await _getSharedPreferencesMap();
completer.complete(SharedPreferences._(preferencesMap)); completer.complete(SharedPreferences._(preferencesMap));
} on Exception catch (e) { } catch (e) {
// If there's an error, explicitly return the future with an error. // If there's an error, explicitly return the future with an error.
// then set the completer to null so we can retry. // then set the completer to null so we can retry.
completer.completeError(e); completer.completeError(e);
@ -74,7 +75,6 @@ class SharedPreferences {
_completer = null; _completer = null;
return sharedPrefsFuture; return sharedPrefsFuture;
} }
_completer = completer;
} }
return _completer!.future; return _completer!.future;
} }

View File

@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs.
Wraps NSUserDefaults on iOS and SharedPreferences on Android. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences 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 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22
version: 2.1.1 version: 2.1.2
environment: environment:
sdk: ">=2.18.0 <4.0.0" sdk: ">=2.18.0 <4.0.0"

View File

@ -10,7 +10,6 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group('SharedPreferences', () {
const String testString = 'hello world'; const String testString = 'hello world';
const bool testBool = true; const bool testBool = true;
const int testInt = 42; const int testInt = 42;
@ -147,8 +146,7 @@ void main() {
await preferences.setString('String', testString); await preferences.setString('String', testString);
expect(preferences.getString('String'), testString); expect(preferences.getString('String'), testString);
SharedPreferences.setMockInitialValues( SharedPreferences.setMockInitialValues(testValues2.cast<String, Object>());
testValues2.cast<String, Object>());
expect(preferences.getString('String'), testString); expect(preferences.getString('String'), testString);
await preferences.reload(); await preferences.reload();
@ -203,7 +201,6 @@ void main() {
expect(preferences.getStringList('myList'), <String>[]); expect(preferences.getStringList('myList'), <String>[]);
}); });
});
test('calling mock initial values with non-prefixed keys succeeds', () async { test('calling mock initial values with non-prefixed keys succeeds', () async {
SharedPreferences.setMockInitialValues(<String, Object>{ SharedPreferences.setMockInitialValues(<String, Object>{
@ -214,6 +211,16 @@ void main() {
expect(value, 'foo'); expect(value, 'foo');
}); });
test('getInstance always returns the same instance', () async {
SharedPreferencesStorePlatform.instance = SlowInitSharedPreferencesStore();
final Future<SharedPreferences> firstFuture =
SharedPreferences.getInstance();
final Future<SharedPreferences> secondFuture =
SharedPreferences.getInstance();
expect(identical(await firstFuture, await secondFuture), true);
});
test('calling setPrefix after getInstance throws', () async { test('calling setPrefix after getInstance throws', () async {
const String newPrefix = 'newPrefix'; const String newPrefix = 'newPrefix';
@ -367,6 +374,15 @@ class UnimplementedSharedPreferencesStore
} }
} }
class SlowInitSharedPreferencesStore
extends UnimplementedSharedPreferencesStore {
@override
Future<Map<String, Object>> getAll() async {
await Future<void>.delayed(const Duration(seconds: 1));
return <String, Object>{};
}
}
class ThrowingSharedPreferencesStore extends SharedPreferencesStorePlatform { class ThrowingSharedPreferencesStore extends SharedPreferencesStorePlatform {
@override @override
Future<bool> clear() { Future<bool> clear() {