Skip to content

[shared_preferences] Fix a late initialized error with the example app #8540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/shared_preferences/shared_preferences/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 2.5.3
* Fixes a bug in the example app.

## 2.5.2

* Fixes `setState` returning `Future` on `example/main.dart` error in example code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
late Future<int> _counter;
int _externalCounter = 0;

/// Completes when the preferences have been initialized, which happens after
/// legacy preferences have been migrated.
final Completer<void> _preferencesReady = Completer<void>();

Future<void> _incrementCounter() async {
final SharedPreferencesWithCache prefs = await _prefs;
final int counter = (prefs.getInt('counter') ?? 0) + 1;
Expand Down Expand Up @@ -86,6 +90,7 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
return prefs.getInt('counter') ?? 0;
});
_getExternalCounter();
_preferencesReady.complete();
});
}

Expand All @@ -96,25 +101,30 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
title: const Text('SharedPreferencesWithCache Demo'),
),
body: Center(
child: FutureBuilder<int>(
future: _counter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return const CircularProgressIndicator();
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text(
'Button tapped ${snapshot.data ?? 0 + _externalCounter} time${(snapshot.data ?? 0 + _externalCounter) == 1 ? '' : 's'}.\n\n'
'This should persist across restarts.',
);
}
}
})),
child: _WaitForInitialization(
initialized: _preferencesReady.future,
builder: (BuildContext context) => FutureBuilder<int>(
future: _counter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return const CircularProgressIndicator();
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text(
'Button tapped ${snapshot.data ?? 0 + _externalCounter} time${(snapshot.data ?? 0 + _externalCounter) == 1 ? '' : 's'}.\n\n'
'This should persist across restarts.',
);
}
}
},
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
Expand All @@ -123,3 +133,28 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
);
}
}

/// Waits for the [initialized] future to complete before rendering [builder].
class _WaitForInitialization extends StatelessWidget {
const _WaitForInitialization({
required this.initialized,
required this.builder,
});

final Future<void> initialized;
final WidgetBuilder builder;

@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: initialized,
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting ||
snapshot.connectionState == ConnectionState.none) {
return const CircularProgressIndicator();
}
return builder(context);
},
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences_example/main.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';

void main() {
group('SharedPreferences example app', () {
setUp(() {
SharedPreferencesAsyncPlatform.instance = FakeSharedPreferencesAsync();
});

tearDown(() {
SharedPreferencesAsyncPlatform.instance = null;
});

testWidgets('builds successfully', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
});
});
}

// Note: this code is duplicated in
// shared_preferences/test/shared_preferences_async_test.dart. Since we cannot
// import the relative path ../../test/shared_preferences_async_test.dart on the
// web platform, we had to copy it here for use in this test library.
Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to put this code somewhere that can be imported in both locations that isn't exported to users?

Copy link
Member Author

@kenzieschmoll kenzieschmoll Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short of creating a shared package for testing utilities (which seems overkill just to share code with the example app test), I'm not sure how else to share this code.

I'm not entirely sure why the relative path can be imported successfully from a test for native platforms but not for the web platform. CC @jakemac53 do you know if this behavior is expected from the test package for this case?

Another option is to add TestOn('vm') here so that this test does not run for the web platform.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected yes, the way apps are hosted on the web we have no way to access URIs outside of the package.

The "root" of the server is either the test directory or the package directory (can't remember which), so you can only access things from the current package and then package: URIs (via synthetic /packages/ paths).

Copy link
Contributor

@jakemac53 jakemac53 Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These sorts of relative paths also really shouldn't be used anyways, they can easily be broken (that file could have dependencies you don't have for instance, and pub won't be able to resolve that for you).

The solution is a shared testing package (no need to publish it).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarrinneal are you okay landing this with a todo to create a shared testing package or do we need to do that as part of this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well :)

base class FakeSharedPreferencesAsync extends SharedPreferencesAsyncPlatform {
final InMemorySharedPreferencesAsync backend =
InMemorySharedPreferencesAsync.empty();
final List<MethodCall> log = <MethodCall>[];

@override
Future<bool> clear(
ClearPreferencesParameters parameters, SharedPreferencesOptions options) {
log.add(MethodCall('clear', <Object>[...?parameters.filter.allowList]));
return backend.clear(parameters, options);
}

@override
Future<bool?> getBool(String key, SharedPreferencesOptions options) {
log.add(MethodCall('getBool', <String>[key]));
return backend.getBool(key, options);
}

@override
Future<double?> getDouble(String key, SharedPreferencesOptions options) {
log.add(MethodCall('getDouble', <String>[key]));
return backend.getDouble(key, options);
}

@override
Future<int?> getInt(String key, SharedPreferencesOptions options) {
log.add(MethodCall('getInt', <String>[key]));
return backend.getInt(key, options);
}

@override
Future<Set<String>> getKeys(
GetPreferencesParameters parameters, SharedPreferencesOptions options) {
log.add(MethodCall('getKeys', <String>[...?parameters.filter.allowList]));
return backend.getKeys(parameters, options);
}

@override
Future<Map<String, Object>> getPreferences(
GetPreferencesParameters parameters, SharedPreferencesOptions options) {
log.add(MethodCall(
'getPreferences', <Object>[...?parameters.filter.allowList]));
return backend.getPreferences(parameters, options);
}

@override
Future<String?> getString(String key, SharedPreferencesOptions options) {
log.add(MethodCall('getString', <String>[key]));
return backend.getString(key, options);
}

@override
Future<List<String>?> getStringList(
String key, SharedPreferencesOptions options) {
log.add(MethodCall('getStringList', <String>[key]));
return backend.getStringList(key, options);
}

@override
Future<bool> setBool(
String key, bool value, SharedPreferencesOptions options) {
log.add(MethodCall('setBool', <Object>[key, value]));
return backend.setBool(key, value, options);
}

@override
Future<bool> setDouble(
String key, double value, SharedPreferencesOptions options) {
log.add(MethodCall('setDouble', <Object>[key, value]));
return backend.setDouble(key, value, options);
}

@override
Future<bool> setInt(String key, int value, SharedPreferencesOptions options) {
log.add(MethodCall('setInt', <Object>[key, value]));
return backend.setInt(key, value, options);
}

@override
Future<bool> setString(
String key, String value, SharedPreferencesOptions options) {
log.add(MethodCall('setString', <Object>[key, value]));
return backend.setString(key, value, options);
}

@override
Future<bool> setStringList(
String key, List<String> value, SharedPreferencesOptions options) {
log.add(MethodCall('setStringList', <Object>[key, value]));
return backend.setStringList(key, value, options);
}
}
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.5.2
version: 2.5.3

environment:
sdk: ^3.5.0
Expand Down