Skip to content

Commit bf794aa

Browse files
kurtisnelsonmehmetf
authored andcommitted
Make SharedPreferences return failures (flutter#441)
On Android, SharedPreferences#apply() will silently fail if there is an issue. Using #commit() ensures we can propogate errors up the future chain. Also updated the example to reflect the fact that preference writes are not instant. Deprecated commit() as neither platform actually implements it correctly.
1 parent 4af10ce commit bf794aa

File tree

6 files changed

+116
-90
lines changed

6 files changed

+116
-90
lines changed

packages/shared_preferences/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
[![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dartlang.org/packages/shared_preferences)
44

55
Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing
6-
a persistent store for simple data. Data is persisted to disk automatically
7-
and asynchronously.
6+
a persistent store for simple data. Data is persisted to disk asynchronously.
7+
Neither platform can guarantee that writes will be persisted to disk after
8+
returning and this plugin must not be used for storing critical data.
89

910
## Usage
1011
To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
@@ -32,7 +33,7 @@ _incrementCounter() async {
3233
SharedPreferences prefs = await SharedPreferences.getInstance();
3334
int counter = (prefs.getInt('counter') ?? 0) + 1;
3435
print('Pressed $counter times.');
35-
prefs.setInt('counter', counter);
36+
await prefs.setInt('counter', counter);
3637
}
3738
```
3839

packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.flutter.plugins.sharedpreferences;
66

77
import android.content.Context;
8+
import android.content.SharedPreferences.Editor;
89
import android.util.Base64;
910
import io.flutter.plugin.common.MethodCall;
1011
import io.flutter.plugin.common.MethodChannel;
@@ -33,7 +34,6 @@ public class SharedPreferencesPlugin implements MethodCallHandler {
3334
private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy";
3435

3536
private final android.content.SharedPreferences preferences;
36-
private final android.content.SharedPreferences.Editor editor;
3737

3838
public static void registerWith(PluginRegistry.Registrar registrar) {
3939
MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
@@ -43,7 +43,6 @@ public static void registerWith(PluginRegistry.Registrar registrar) {
4343

4444
private SharedPreferencesPlugin(Context context) {
4545
preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
46-
editor = preferences.edit();
4746
}
4847

4948
private List<String> decodeList(String encodedList) throws IOException {
@@ -94,8 +93,17 @@ private Map<String, Object> getAllPrefs() throws IOException {
9493
// This only happens for previous usage of setStringSet. The app expects a list.
9594
List<String> listValue = new ArrayList<>((Set) value);
9695
// Let's migrate the value too while we are at it.
97-
editor.remove(key);
98-
editor.putString(key, LIST_IDENTIFIER + encodeList(listValue)).apply();
96+
boolean success =
97+
preferences
98+
.edit()
99+
.remove(key)
100+
.putString(key, LIST_IDENTIFIER + encodeList(listValue))
101+
.commit();
102+
if (!success) {
103+
// If we are unable to migrate the existing preferences, it means we potentially lost them.
104+
// In this case, an error from getAllPrefs() is appropriate since it will alert the app during plugin initialization.
105+
throw new IOException("Could not migrate set to list");
106+
}
99107
value = listValue;
100108
}
101109
filteredPrefs.put(key, value);
@@ -107,57 +115,57 @@ private Map<String, Object> getAllPrefs() throws IOException {
107115
@Override
108116
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
109117
String key = call.argument("key");
118+
boolean status = false;
110119
try {
111120
switch (call.method) {
112121
case "setBool":
113-
editor.putBoolean(key, (boolean) call.argument("value")).apply();
114-
result.success(null);
122+
status = preferences.edit().putBoolean(key, (boolean) call.argument("value")).commit();
115123
break;
116124
case "setDouble":
117125
float floatValue = ((Number) call.argument("value")).floatValue();
118-
editor.putFloat(key, floatValue).apply();
119-
result.success(null);
126+
status = preferences.edit().putFloat(key, floatValue).commit();
120127
break;
121128
case "setInt":
122129
Number number = call.argument("value");
130+
Editor editor = preferences.edit();
123131
if (number instanceof BigInteger) {
124132
BigInteger integerValue = (BigInteger) number;
125133
editor.putString(key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX));
126134
} else {
127135
editor.putLong(key, number.longValue());
128136
}
129-
editor.apply();
130-
result.success(null);
137+
status = editor.commit();
131138
break;
132139
case "setString":
133-
editor.putString(key, (String) call.argument("value")).apply();
134-
result.success(null);
140+
status = preferences.edit().putString(key, (String) call.argument("value")).commit();
135141
break;
136142
case "setStringList":
137143
List<String> list = call.argument("value");
138-
editor.putString(key, LIST_IDENTIFIER + encodeList(list)).apply();
139-
result.success(null);
144+
status = preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)).commit();
140145
break;
141146
case "commit":
142-
result.success(editor.commit());
147+
// We've been committing the whole time.
148+
status = true;
143149
break;
144150
case "getAll":
145151
result.success(getAllPrefs());
146-
break;
152+
return;
147153
case "remove":
148-
editor.remove(key).apply();
149-
result.success(null);
154+
status = preferences.edit().remove(key).commit();
150155
break;
151156
case "clear":
152-
for (String keyToDelete : getAllPrefs().keySet()) {
153-
editor.remove(keyToDelete);
157+
Set<String> keySet = getAllPrefs().keySet();
158+
Editor clearEditor = preferences.edit();
159+
for (String keyToDelete : keySet) {
160+
clearEditor.remove(keyToDelete);
154161
}
155-
result.success(editor.commit());
162+
status = clearEditor.commit();
156163
break;
157164
default:
158165
result.notImplemented();
159166
break;
160167
}
168+
result.success(status);
161169
} catch (IOException e) {
162170
result.error("IOException encountered", call.method, e);
163171
}

packages/shared_preferences/example/lib/main.dart

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,24 @@ class SharedPreferencesDemo extends StatefulWidget {
3030

3131
class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
3232
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
33+
Future<int> _counter;
3334

3435
Future<Null> _incrementCounter() async {
3536
final SharedPreferences prefs = await _prefs;
3637
final int counter = (prefs.getInt('counter') ?? 0) + 1;
38+
3739
setState(() {
38-
prefs.setInt("counter", counter);
40+
_counter = prefs.setInt("counter", counter).then((bool success) {
41+
return counter;
42+
});
43+
});
44+
}
45+
46+
@override
47+
void initState() {
48+
super.initState();
49+
_counter = _prefs.then((SharedPreferences prefs) {
50+
return (prefs.getInt('counter') ?? 0);
3951
});
4052
}
4153

@@ -46,18 +58,21 @@ class SharedPreferencesDemoState extends State<SharedPreferencesDemo> {
4658
title: const Text("SharedPreferences Demo"),
4759
),
4860
body: new Center(
49-
child: new FutureBuilder<SharedPreferences>(
50-
future: _prefs,
51-
builder: (BuildContext context,
52-
AsyncSnapshot<SharedPreferences> snapshot) {
53-
if (snapshot.connectionState == ConnectionState.waiting)
54-
return const Text('Loading...');
55-
final int counter = snapshot.requireData.getInt('counter') ?? 0;
56-
// ignore: prefer_const_constructors
57-
return new Text(
58-
'Button tapped $counter time${ counter == 1 ? '' : 's' }.\n\n'
59-
'This should persist across restarts.',
60-
);
61+
child: new FutureBuilder<int>(
62+
future: _counter,
63+
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
64+
switch (snapshot.connectionState) {
65+
case ConnectionState.waiting:
66+
return const CircularProgressIndicator();
67+
default:
68+
if (snapshot.hasError)
69+
return new Text('Error: ${snapshot.error}');
70+
else
71+
return new Text(
72+
'Button tapped ${snapshot.data} time${ snapshot.data == 1 ? '' : 's' }.\n\n'
73+
'This should persist across restarts.',
74+
);
75+
}
6176
})),
6277
floatingActionButton: new FloatingActionButton(
6378
onPressed: _incrementCounter,

packages/shared_preferences/ios/Classes/SharedPreferencesPlugin.m

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,43 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
2121
NSString *key = arguments[@"key"];
2222
NSNumber *value = arguments[@"value"];
2323
[[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key];
24-
result(nil);
24+
result(@YES);
2525
} else if ([method isEqualToString:@"setInt"]) {
2626
NSString *key = arguments[@"key"];
2727
NSNumber *value = arguments[@"value"];
2828
// int type in Dart can come to native side in a variety of forms
2929
// It is best to store it as is and send it back when needed.
3030
// Platform channel will handle the conversion.
3131
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
32-
result(nil);
32+
result(@YES);
3333
} else if ([method isEqualToString:@"setDouble"]) {
3434
NSString *key = arguments[@"key"];
3535
NSNumber *value = arguments[@"value"];
3636
[[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key];
37-
result(nil);
37+
result(@YES);
3838
} else if ([method isEqualToString:@"setString"]) {
3939
NSString *key = arguments[@"key"];
4040
NSString *value = arguments[@"value"];
4141
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
42-
result(nil);
42+
result(@YES);
4343
} else if ([method isEqualToString:@"setStringList"]) {
4444
NSString *key = arguments[@"key"];
4545
NSArray *value = arguments[@"value"];
4646
[[NSUserDefaults standardUserDefaults] setValue:value forKey:key];
47-
result(nil);
47+
result(@YES);
4848
} else if ([method isEqualToString:@"commit"]) {
49-
result([NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] synchronize]]);
49+
// synchronize is deprecated.
50+
// "this method is unnecessary and shouldn't be used."
51+
result(@YES);
5052
} else if ([method isEqualToString:@"remove"]) {
5153
[[NSUserDefaults standardUserDefaults] removeObjectForKey:arguments[@"key"]];
52-
result(nil);
54+
result(@YES);
5355
} else if ([method isEqualToString:@"clear"]) {
5456
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
5557
for (NSString *key in getAllPrefs()) {
5658
[defaults removeObjectForKey:key];
5759
}
58-
result([NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] synchronize]]);
60+
result(@YES);
5961
} else {
6062
result(FlutterMethodNotImplemented);
6163
}

packages/shared_preferences/lib/shared_preferences.dart

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ const MethodChannel _kChannel =
1313
/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing
1414
/// a persistent store for simple data.
1515
///
16-
/// Data is persisted to disk automatically and asynchronously. Use [commit()]
17-
/// to be notified when a save is successful.
16+
/// Data is persisted to disk asynchronously.
1817
class SharedPreferences {
1918
SharedPreferences._(this._preferenceCache);
2019

@@ -79,56 +78,57 @@ class SharedPreferences {
7978
/// Saves a boolean [value] to persistent storage in the background.
8079
///
8180
/// If [value] is null, this is equivalent to calling [remove()] on the [key].
82-
void setBool(String key, bool value) => _setValue('Bool', key, value);
81+
Future<bool> setBool(String key, bool value) => _setValue('Bool', key, value);
8382

8483
/// Saves an integer [value] to persistent storage in the background.
8584
///
8685
/// If [value] is null, this is equivalent to calling [remove()] on the [key].
87-
void setInt(String key, int value) => _setValue('Int', key, value);
86+
Future<bool> setInt(String key, int value) => _setValue('Int', key, value);
8887

8988
/// Saves a double [value] to persistent storage in the background.
9089
///
9190
/// Android doesn't support storing doubles, so it will be stored as a float.
9291
///
9392
/// If [value] is null, this is equivalent to calling [remove()] on the [key].
94-
void setDouble(String key, double value) => _setValue('Double', key, value);
93+
Future<bool> setDouble(String key, double value) =>
94+
_setValue('Double', key, value);
9595

9696
/// Saves a string [value] to persistent storage in the background.
9797
///
9898
/// If [value] is null, this is equivalent to calling [remove()] on the [key].
99-
void setString(String key, String value) => _setValue('String', key, value);
99+
Future<bool> setString(String key, String value) =>
100+
_setValue('String', key, value);
100101

101102
/// Saves a list of strings [value] to persistent storage in the background.
102103
///
103104
/// If [value] is null, this is equivalent to calling [remove()] on the [key].
104-
void setStringList(String key, List<String> value) =>
105+
Future<bool> setStringList(String key, List<String> value) =>
105106
_setValue('StringList', key, value);
106107

107108
/// Removes an entry from persistent storage.
108-
void remove(String key) => _setValue(null, key, null);
109+
Future<bool> remove(String key) => _setValue(null, key, null);
109110

110-
void _setValue(String valueType, String key, Object value) {
111-
// Set the value in the background.
111+
Future<bool> _setValue(String valueType, String key, Object value) {
112112
final Map<String, dynamic> params = <String, dynamic>{
113113
'key': '$_prefix$key',
114114
};
115115
if (value == null) {
116116
_preferenceCache.remove(key);
117-
_kChannel.invokeMethod('remove', params);
117+
return _kChannel
118+
.invokeMethod('remove', params)
119+
.then<bool>((dynamic result) => result);
118120
} else {
119121
_preferenceCache[key] = value;
120122
params['value'] = value;
121-
_kChannel.invokeMethod('set$valueType', params);
123+
return _kChannel
124+
.invokeMethod('set$valueType', params)
125+
.then<bool>((dynamic result) => result);
122126
}
123127
}
124128

125-
/// Completes with true once saved values have been persisted to local
126-
/// storage, or false if the save failed.
127-
///
128-
/// It's usually sufficient to just wait for the set methods to complete which
129-
/// ensure the preferences have been modified in memory. Commit is necessary
130-
/// only if you need to be absolutely sure that the data is in persistent
131-
/// storage before taking some other action.
129+
/// Always returns true.
130+
/// On iOS, synchronize is marked deprecated. On Android, we commit every set.
131+
@deprecated
132132
Future<bool> commit() async => await _kChannel.invokeMethod('commit');
133133

134134
/// Completes with true once the user preferences for the app has been cleared.

0 commit comments

Comments
 (0)