Skip to content

Commit 064b0b2

Browse files
[shared_preferences] Fix a late initialized error with the example app (#8540)
The `_counter` future was throwing a late initialization error because it was accessed from the `FutureBuilder` in `build()` before it was initialized. This adds a completer `_preferencesReady` that we wait for before trying to build the `FutureBuilder` for `_counter`.
1 parent 279dda8 commit 064b0b2

File tree

4 files changed

+181
-20
lines changed

4 files changed

+181
-20
lines changed

packages/shared_preferences/shared_preferences/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 2.5.3
2+
* Fixes a bug in the example app.
3+
14
## 2.5.2
25

36
* Fixes `setState` returning `Future` on `example/main.dart` error in example code.

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

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
4444
late Future<int> _counter;
4545
int _externalCounter = 0;
4646

47+
/// Completes when the preferences have been initialized, which happens after
48+
/// legacy preferences have been migrated.
49+
final Completer<void> _preferencesReady = Completer<void>();
50+
4751
Future<void> _incrementCounter() async {
4852
final SharedPreferencesWithCache prefs = await _prefs;
4953
final int counter = (prefs.getInt('counter') ?? 0) + 1;
@@ -86,6 +90,7 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
8690
return prefs.getInt('counter') ?? 0;
8791
});
8892
_getExternalCounter();
93+
_preferencesReady.complete();
8994
});
9095
}
9196

@@ -96,25 +101,30 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
96101
title: const Text('SharedPreferencesWithCache Demo'),
97102
),
98103
body: Center(
99-
child: FutureBuilder<int>(
100-
future: _counter,
101-
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
102-
switch (snapshot.connectionState) {
103-
case ConnectionState.none:
104-
case ConnectionState.waiting:
105-
return const CircularProgressIndicator();
106-
case ConnectionState.active:
107-
case ConnectionState.done:
108-
if (snapshot.hasError) {
109-
return Text('Error: ${snapshot.error}');
110-
} else {
111-
return Text(
112-
'Button tapped ${snapshot.data ?? 0 + _externalCounter} time${(snapshot.data ?? 0 + _externalCounter) == 1 ? '' : 's'}.\n\n'
113-
'This should persist across restarts.',
114-
);
115-
}
116-
}
117-
})),
104+
child: _WaitForInitialization(
105+
initialized: _preferencesReady.future,
106+
builder: (BuildContext context) => FutureBuilder<int>(
107+
future: _counter,
108+
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
109+
switch (snapshot.connectionState) {
110+
case ConnectionState.none:
111+
case ConnectionState.waiting:
112+
return const CircularProgressIndicator();
113+
case ConnectionState.active:
114+
case ConnectionState.done:
115+
if (snapshot.hasError) {
116+
return Text('Error: ${snapshot.error}');
117+
} else {
118+
return Text(
119+
'Button tapped ${snapshot.data ?? 0 + _externalCounter} time${(snapshot.data ?? 0 + _externalCounter) == 1 ? '' : 's'}.\n\n'
120+
'This should persist across restarts.',
121+
);
122+
}
123+
}
124+
},
125+
),
126+
),
127+
),
118128
floatingActionButton: FloatingActionButton(
119129
onPressed: _incrementCounter,
120130
tooltip: 'Increment',
@@ -123,3 +133,28 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
123133
);
124134
}
125135
}
136+
137+
/// Waits for the [initialized] future to complete before rendering [builder].
138+
class _WaitForInitialization extends StatelessWidget {
139+
const _WaitForInitialization({
140+
required this.initialized,
141+
required this.builder,
142+
});
143+
144+
final Future<void> initialized;
145+
final WidgetBuilder builder;
146+
147+
@override
148+
Widget build(BuildContext context) {
149+
return FutureBuilder<void>(
150+
future: initialized,
151+
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
152+
if (snapshot.connectionState == ConnectionState.waiting ||
153+
snapshot.connectionState == ConnectionState.none) {
154+
return const CircularProgressIndicator();
155+
}
156+
return builder(context);
157+
},
158+
);
159+
}
160+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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:flutter/services.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:shared_preferences_example/main.dart';
8+
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
9+
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
10+
import 'package:shared_preferences_platform_interface/types.dart';
11+
12+
void main() {
13+
group('SharedPreferences example app', () {
14+
setUp(() {
15+
SharedPreferencesAsyncPlatform.instance = FakeSharedPreferencesAsync();
16+
});
17+
18+
tearDown(() {
19+
SharedPreferencesAsyncPlatform.instance = null;
20+
});
21+
22+
testWidgets('builds successfully', (WidgetTester tester) async {
23+
await tester.pumpWidget(const MyApp());
24+
});
25+
});
26+
}
27+
28+
// Note: this code is duplicated in
29+
// shared_preferences/test/shared_preferences_async_test.dart. Since we cannot
30+
// import the relative path ../../test/shared_preferences_async_test.dart on the
31+
// web platform, we had to copy it here for use in this test library.
32+
base class FakeSharedPreferencesAsync extends SharedPreferencesAsyncPlatform {
33+
final InMemorySharedPreferencesAsync backend =
34+
InMemorySharedPreferencesAsync.empty();
35+
final List<MethodCall> log = <MethodCall>[];
36+
37+
@override
38+
Future<bool> clear(
39+
ClearPreferencesParameters parameters, SharedPreferencesOptions options) {
40+
log.add(MethodCall('clear', <Object>[...?parameters.filter.allowList]));
41+
return backend.clear(parameters, options);
42+
}
43+
44+
@override
45+
Future<bool?> getBool(String key, SharedPreferencesOptions options) {
46+
log.add(MethodCall('getBool', <String>[key]));
47+
return backend.getBool(key, options);
48+
}
49+
50+
@override
51+
Future<double?> getDouble(String key, SharedPreferencesOptions options) {
52+
log.add(MethodCall('getDouble', <String>[key]));
53+
return backend.getDouble(key, options);
54+
}
55+
56+
@override
57+
Future<int?> getInt(String key, SharedPreferencesOptions options) {
58+
log.add(MethodCall('getInt', <String>[key]));
59+
return backend.getInt(key, options);
60+
}
61+
62+
@override
63+
Future<Set<String>> getKeys(
64+
GetPreferencesParameters parameters, SharedPreferencesOptions options) {
65+
log.add(MethodCall('getKeys', <String>[...?parameters.filter.allowList]));
66+
return backend.getKeys(parameters, options);
67+
}
68+
69+
@override
70+
Future<Map<String, Object>> getPreferences(
71+
GetPreferencesParameters parameters, SharedPreferencesOptions options) {
72+
log.add(MethodCall(
73+
'getPreferences', <Object>[...?parameters.filter.allowList]));
74+
return backend.getPreferences(parameters, options);
75+
}
76+
77+
@override
78+
Future<String?> getString(String key, SharedPreferencesOptions options) {
79+
log.add(MethodCall('getString', <String>[key]));
80+
return backend.getString(key, options);
81+
}
82+
83+
@override
84+
Future<List<String>?> getStringList(
85+
String key, SharedPreferencesOptions options) {
86+
log.add(MethodCall('getStringList', <String>[key]));
87+
return backend.getStringList(key, options);
88+
}
89+
90+
@override
91+
Future<bool> setBool(
92+
String key, bool value, SharedPreferencesOptions options) {
93+
log.add(MethodCall('setBool', <Object>[key, value]));
94+
return backend.setBool(key, value, options);
95+
}
96+
97+
@override
98+
Future<bool> setDouble(
99+
String key, double value, SharedPreferencesOptions options) {
100+
log.add(MethodCall('setDouble', <Object>[key, value]));
101+
return backend.setDouble(key, value, options);
102+
}
103+
104+
@override
105+
Future<bool> setInt(String key, int value, SharedPreferencesOptions options) {
106+
log.add(MethodCall('setInt', <Object>[key, value]));
107+
return backend.setInt(key, value, options);
108+
}
109+
110+
@override
111+
Future<bool> setString(
112+
String key, String value, SharedPreferencesOptions options) {
113+
log.add(MethodCall('setString', <Object>[key, value]));
114+
return backend.setString(key, value, options);
115+
}
116+
117+
@override
118+
Future<bool> setStringList(
119+
String key, List<String> value, SharedPreferencesOptions options) {
120+
log.add(MethodCall('setStringList', <Object>[key, value]));
121+
return backend.setStringList(key, value, options);
122+
}
123+
}

packages/shared_preferences/shared_preferences/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs.
33
Wraps NSUserDefaults on iOS and SharedPreferences on Android.
44
repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22
6-
version: 2.5.2
6+
version: 2.5.3
77

88
environment:
99
sdk: ^3.5.0

0 commit comments

Comments
 (0)