Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Web implementation of shared_preferences #2332

Merged
merged 5 commits into from Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,3 @@
# 0.0.1
ditman marked this conversation as resolved.
Show resolved Hide resolved

- Initial release.
27 changes: 27 additions & 0 deletions 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.
32 changes: 32 additions & 0 deletions 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.0.1
Copy link
Contributor

Choose a reason for hiding this comment

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

update this line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Done.

...
```

### 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
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to prepend something to the user-supplied key, to minimize collisions with other things that might be using the shared local storage? shared_preferences or similar?

We can make this transparent on all add/remove operations so users don't even see that we're doing this (unless they go to the dev tools and peek :P)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The SharedPreferences class from package:shared_preferences already prepends "flutter.". So here I'm just verifying that it's prefixed in _checkPrefix. On top of that, for security reasons, localStorage is scoped to the domain, so you can't step on another site's data.

We could allow customizing the prefix, but currently the value is hard-coded across Dart, Java, and Objective-C code, so it would be a much bigger change.

Copy link
Member

Choose a reason for hiding this comment

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

Ah ok, I thought this relied on the user prepending "flutter." to the key themselves. If it's the shared_prefs plugin, I guess it's OK.

I know you can't step on other's site data; the scenario I was envisioning was multiple plugins writing to localStorage on the same site, and overwriting each other's data.

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>();
}
ditman marked this conversation as resolved.
Show resolved Hide resolved

return decodedValue;
}
}
28 changes: 28 additions & 0 deletions 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.0.1
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: prefer the initial release to be 0.1.0 to get a better score on pub

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


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"
Copy link
Member

Choose a reason for hiding this comment

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

We haven't been too strict with this so far, but I think that you need a newer version of flutter to support the plugin.platforms syntax of the pubspec.yaml.

I'm not sure what's a good value for the minimal version of flutter yet, though :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a copy'n'paste from url_launcher. LMK what I should use here instead.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not entirely sure, I have a feeling we'll end up coming and updating all of these to the proper version later on, don't worry too much! :P

@@ -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
ditman marked this conversation as resolved.
Show resolved Hide resolved

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>>());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ditman this test here shows how the generics are preserved. If you change this to isA<List<int>>(), the test will fail.

Copy link
Member

Choose a reason for hiding this comment

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

This is a functionality from the language that I wasn't expecting at all!


// 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');
});
});
}