Web implementation of shared_preferences (#2332)
Implement `shared_preferences` for the Web.
diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md
new file mode 100644
index 0000000..08378c6
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.1.0
+
+- Initial release.
diff --git a/packages/shared_preferences/shared_preferences_web/LICENSE b/packages/shared_preferences/shared_preferences_web/LICENSE
new file mode 100644
index 0000000..0c382ce
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/shared_preferences/shared_preferences_web/README.md b/packages/shared_preferences/shared_preferences_web/README.md
new file mode 100644
index 0000000..8f9d22d
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/README.md
@@ -0,0 +1,32 @@
+# shared_preferences_web
+
+The web implementation of [`shared_preferences`][1].
+
+## Usage
+
+### Import the package
+
+To use this plugin in your Flutter Web app, simply add it as a dependency in
+your `pubspec.yaml` alongside the base `shared_preferences` plugin.
+
+_(This is only temporary: in the future we hope to make this package an
+"endorsed" implementation of `shared_preferences`, so that it is automatically
+included in your Flutter Web app when you depend on `package:shared_preferences`.)_
+
+This is what the above means to your `pubspec.yaml`:
+
+```yaml
+...
+dependencies:
+ ...
+ shared_preferences: ^0.5.4+8
+ shared_preferences_web: ^0.1.0
+ ...
+```
+
+### Use the plugin
+
+Once you have the `shared_preferences_web` dependency in your pubspec, you should
+be able to use `package:shared_preferences` as normal.
+
+[1]: ../shared_preferences/shared_preferences
diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart
new file mode 100644
index 0000000..8a0f137
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart
@@ -0,0 +1,91 @@
+// Copyright 2017 The Chromium 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 'dart:async';
+import 'dart:convert' show json;
+import 'dart:html' as html;
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
+
+/// The web implementation of [SharedPreferencesStorePlatform].
+///
+/// This class implements the `package:shared_preferences` functionality for the web.
+class SharedPreferencesPlugin extends SharedPreferencesStorePlatform {
+ /// Registers this class as the default instance of [SharedPreferencesStorePlatform].
+ static void registerWith(Registrar registrar) {
+ SharedPreferencesStorePlatform.instance = SharedPreferencesPlugin();
+ }
+
+ @override
+ Future<bool> clear() async {
+ // IMPORTANT: Do not use html.window.localStorage.clear() as that will
+ // remove _all_ local data, not just the keys prefixed with
+ // "flutter."
+ for (String key in _storedFlutterKeys) {
+ html.window.localStorage.remove(key);
+ }
+ return true;
+ }
+
+ @override
+ Future<Map<String, Object>> getAll() async {
+ final Map<String, Object> allData = <String, Object>{};
+ for (String key in _storedFlutterKeys) {
+ allData[key] = _decodeValue(html.window.localStorage[key]);
+ }
+ return allData;
+ }
+
+ @override
+ Future<bool> remove(String key) async {
+ _checkPrefix(key);
+ html.window.localStorage.remove(key);
+ return true;
+ }
+
+ @override
+ Future<bool> setValue(String valueType, String key, Object value) async {
+ _checkPrefix(key);
+ html.window.localStorage[key] = _encodeValue(value);
+ return true;
+ }
+
+ void _checkPrefix(String key) {
+ if (!key.startsWith('flutter.')) {
+ throw FormatException(
+ 'Shared preferences keys must start with prefix "flutter.".',
+ key,
+ 0,
+ );
+ }
+ }
+
+ List<String> get _storedFlutterKeys {
+ final List<String> keys = <String>[];
+ for (String key in html.window.localStorage.keys) {
+ if (key.startsWith('flutter.')) {
+ keys.add(key);
+ }
+ }
+ return keys;
+ }
+
+ String _encodeValue(Object value) {
+ return json.encode(value);
+ }
+
+ Object _decodeValue(String encodedValue) {
+ final Object decodedValue = json.decode(encodedValue);
+
+ if (decodedValue is List) {
+ // JSON does not preserve generics. The encode/decode roundtrip is
+ // `List<String>` => JSON => `List<dynamic>`. We have to explicitly
+ // restore the RTTI.
+ return decodedValue.cast<String>();
+ }
+
+ return decodedValue;
+ }
+}
diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml
new file mode 100644
index 0000000..b2ba90a
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml
@@ -0,0 +1,28 @@
+name: shared_preferences_web
+description: Web platform implementation of shared_preferences
+author: Flutter Team <flutter-dev@googlegroups.com>
+homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web
+version: 0.1.0
+
+flutter:
+ plugin:
+ platforms:
+ web:
+ pluginClass: SharedPreferencesPlugin
+ fileName: shared_preferences_web.dart
+
+dependencies:
+ shared_preferences_platform_interface: ^1.0.0
+ flutter:
+ sdk: flutter
+ flutter_web_plugins:
+ sdk: flutter
+ meta: ^1.1.7
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+environment:
+ sdk: ">=2.0.0-dev.28.0 <3.0.0"
+ flutter: ">=1.5.0 <2.0.0"
diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart
new file mode 100644
index 0000000..951f04c
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert' show json;
+import 'dart:html' as html;
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
+import 'package:shared_preferences_web/shared_preferences_web.dart';
+
+const Map<String, dynamic> kTestValues = <String, dynamic>{
+ 'flutter.String': 'hello world',
+ 'flutter.Bool': true,
+ 'flutter.Int': 42,
+ 'flutter.Double': 3.14159,
+ 'flutter.StringList': <String>['foo', 'bar'],
+};
+
+void main() {
+ group('SharedPreferencesPlugin', () {
+ setUp(() {
+ html.window.localStorage.clear();
+ });
+
+ test('registers itself', () {
+ expect(SharedPreferencesStorePlatform.instance,
+ isNot(isA<SharedPreferencesPlugin>()));
+ SharedPreferencesPlugin.registerWith(null);
+ expect(SharedPreferencesStorePlatform.instance,
+ isA<SharedPreferencesPlugin>());
+ });
+
+ test('getAll', () async {
+ final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+ expect(await store.getAll(), isEmpty);
+
+ html.window.localStorage['flutter.testKey'] = '"test value"';
+ html.window.localStorage['unprefixed_key'] = 'not a flutter value';
+ final Map<String, Object> allData = await store.getAll();
+ expect(allData, hasLength(1));
+ expect(allData['flutter.testKey'], 'test value');
+ });
+
+ test('remove', () async {
+ final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+ html.window.localStorage['flutter.testKey'] = '"test value"';
+ expect(html.window.localStorage['flutter.testKey'], isNotNull);
+ expect(await store.remove('flutter.testKey'), isTrue);
+ expect(html.window.localStorage['flutter.testKey'], isNull);
+ expect(
+ () => store.remove('unprefixed'),
+ throwsA(isA<FormatException>()),
+ );
+ });
+
+ test('setValue', () async {
+ final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+ for (String key in kTestValues.keys) {
+ final dynamic value = kTestValues[key];
+ expect(await store.setValue(key.split('.').last, key, value), true);
+ }
+ expect(html.window.localStorage.keys, hasLength(kTestValues.length));
+ for (String key in html.window.localStorage.keys) {
+ expect(html.window.localStorage[key], json.encode(kTestValues[key]));
+ }
+
+ // Check that generics are preserved.
+ expect((await store.getAll())['flutter.StringList'], isA<List<String>>());
+
+ // Invalid key format.
+ expect(
+ () => store.setValue('String', 'unprefixed', 'hello'),
+ throwsA(isA<FormatException>()),
+ );
+ });
+
+ test('clear', () async {
+ final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+ html.window.localStorage['flutter.testKey1'] = '"test value"';
+ html.window.localStorage['flutter.testKey2'] = '42';
+ html.window.localStorage['unprefixed_key'] = 'not a flutter value';
+ expect(await store.clear(), isTrue);
+ expect(html.window.localStorage.keys.single, 'unprefixed_key');
+ });
+ });
+}