GithubHelp home page GithubHelp logo

roughike / streaming_shared_preferences Goto Github PK

View Code? Open in Web Editor NEW
247.0 10.0 27.0 267 KB

A reactive key-value store for Flutter projects. Like shared_preferences, but with Streams.

License: BSD 2-Clause "Simplified" License

Dart 94.84% Ruby 4.11% Objective-C 0.04% Kotlin 0.52% Swift 0.48%

streaming_shared_preferences's Introduction

streaming_shared_preferences

pub package Build Status Coverage Status

A reactive key-value store for Flutter projects.

streaming_shared_preferences adds reactive functionality on top of shared_preferences. It does everything that regular SharedPreferences does, but it also allows listening to changes in values. This makes it super easy to keep your widgets in sync with persisted values.

Getting started

First, add streaming_shared_preferences into your pubspec.yaml.

If you're already using shared_preferences, you should replace the dependency with streaming_shared_preferences.

dependencies:
  streaming_shared_preferences: ^2.0.0

To get a hold of StreamingSharedPreferences, await on instance:

import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';

...
WidgetsFlutterBinding.ensureInitialized();
final preferences = await StreamingSharedPreferences.instance;

Caveat: The change detection works only in Dart side. This means that if you want to react to changes in values, you should always use StreamingSharedPreferences (not SharedPreferences) to store your values.

Your first streaming preference

Here's the simplest possible plain Dart example on how you would print a value to console every time a counter integer changes:

// Get a reference to the counter value and provide a default value 
// of 0 in case it is null.
Preference<int> counter = preferences.getInt('counter', defaultValue: 0);

// "counter" is a Preference<int> - it can do anything a Stream<int> can.
// We're just going to listen to it and print the value to console.
counter.listen((value) {
  print(value);
});

// Somewhere else in your code, update the value.
counter.setValue(1);

// This is exactly same as above, but the above is more convenient.
preferences.setInt('counter', 2);

The public API follows the same convention as regular SharedPreferences, but every getter returns a Preference object which is a special type of Stream.

Assuming that there's no previously stored value (=it's null), the above example will print 0, 1 and 2 to the console.

Getting a value synchronously

No problem! Just call getValue() on whatever the preferences.getInt(..) (or getString(), getBool(), etc.) returns you.

Connecting values to Flutter widgets

Althought it works perfectly fine with a StreamBuilder, the recommended way is to use the PreferenceBuilder widget.

If you have only one value you need to store in your app, it might make sense to listen to it inline:

class MyCounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// PreferenceBuilder is like StreamBuilder, but with less boilerplate.
    ///
    /// We don't have to provide `initialData` because it can be fetched synchronously
    /// from the provided Preference. There's also no initial flicker when transitioning
    /// between initialData and the stream.
    ///
    /// If you want, you could use a StreamBuilder too.
    return PreferenceBuilder<int>(
      preference: preferences.getInt('counter', defaultValue: 0),
      builder: (BuildContext context, int counter) {
        return Text('Button pressed $counter times!');
      }
    );
  }
}

Use a wrapper class when having multiple preferences

If you have multiple preferences, the recommended approach is to create a class that holds all your Preference objects in a single place:

/// A class that holds [Preference] objects for the common values that you want
/// to store in your app. This is *not* necessarily needed, but it makes your
/// code more neat and fool-proof.
class MyAppSettings {
  MyAppSettings(StreamingSharedPreferences preferences)
      : counter = preferences.getInt('counter', defaultValue: 0),
        nickname = preferences.getString('nickname', defaultValue: '');

  final Preference<int> counter;
  final Preference<String> nickname;
}

In our app entry point, you'll obtain an instance to StreamingSharedPreferences once and pass that to our settings class. Now we can pass MyAppSettings down to the widgets that use it:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  /// Obtain instance to streaming shared preferences, create MyAppSettings, and
  /// once that's done, run the app.
  final preferences = await StreamingSharedPreferences.instance;
  final settings = MyAppSettings(preferences);
  
  runApp(MyApp(settings));
}

This makes the calling code become quite neat:

class MyCounterWidget extends StatelessWidget {
  MyCounterWidget(this.settings);
  final MyAppSettings settings;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        PreferenceBuilder<String>(
          preference: settings.nickname,
          builder: (context, nickname) => Text('Hey $nickname!'),
        ),
        PreferenceBuilder<int>(
          preference: settings.counter,
          builder: (context, counter) => Text('You have pushed the button $counter times!'),
        ),
        FloatingActionButton(
          onPressed: () {
            /// To obtain the current value synchronously, we can call ".getValue()".
            final currentCounter = settings.counter.getValue();

            /// To update the value, we can call ".setValue()" - no need to provide a key!
            /// Alternatively, we could just call "preferences.setInt('counter', currentCounter + 1)".
            settings.counter.setValue(currentCounter + 1);
          },
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}

You can see a full working example of this in the example project.

When your widget hierarchy becomes deep enough, you would want to pass MyAppSettings around with an InheritedWidget or provider instead.

"But what about muh abstraction!"

If you're all about the clean architecture and don't want to pollute your domain layer with Preference objects from a third-party library by some random internet stranger, all the power to you.

Here's one way to make Uncle Bob proud.

/// The contract for persistent values in your app that can be shared
/// to your pure business logic classes
abstract class SettingsContract {
  Stream<int> streamCounterValues();
  void setCounter(int value);
}

/// ... somewhere else in your codebase
class MyBusinessLogic {
  MyBusinessLogic(SettingsContract settings) {
    // Do something with "streamCounterValues()" and "setCounter()" along with
    // whatever your business use case needs
  }
}

No StreamingSharedPreferences specifics went in there.

If for some reason you want to switch into some other library (or get rid of this library altogether), you can do so without modifying your business logic.

Here's how the implementation based on StreamingSharedPreferences would look like:

/// One implementation of SettingsContract backed by StreamingSharedPreferences
class MyAppSettings implements SettingsContract {
  MyAppSettings(StreamingSharedPreferences preferences)
      : counter = preferences.getInt('counter', defaultValue: 0);

  final Preference<int> _counter;

  @override
  Stream<int> streamCounterValues() => _counter;

  @override
  void setCounter(int value) => _counter.setValue(value);
}

Storing custom types with JsonAdapter

The entire library is built to support storing custom data types easily with a PreferenceAdapter. In fact, every built-in type has its own PreferenceAdapter - so every type is actually a custom value.

For most cases, there's a convenience adapter that handles common pitfalls when storing and retrieving custom values called JsonAdapter. It helps you to store your values in JSON and it also saves you from duplicating if (value == null) return null for your custom adapters.

For example, if we have a class called SampleObject:

class SampleObject {
  SampleObject(this.isAwesome);
  final bool isAwesome;

  SampleObject.fromJson(Map<String, dynamic> json) :
    isAwesome = json['isAwesome'];

  Map<String, dynamic> toJson() => { 'isAwesome': isAwesome };
}

As seen from the above example, SampleObject implements both fromJson and toJson.

When the toJson method is present, JsonAdapter will call toJson automatically. For reviving, you need to provide a deserializer that calls fromJson manually:

final sampleObject = preferences.getCustomValue<SampleObject>(
  'my-key',
  defaultValue: SampleObject.empty(),
  adapter: JsonAdapter(
    deserializer: (value) => SampleObject.fromJson(value),
  ),
);

Depending on your use case, you need to provide a non-null SampleObject.empty() that represents a sane default for your custom type when the value is not loaded just yet.

Using JsonAdapter with built_value

You can do custom serialization logic before JSON encoding the object by providing a serializer. Similarly, you can use deserializer to map the decoded JSON map into any object you want.

For example, if the previous SampleObject didn't have toJson and fromJson methods, but was a built_value model instead:

final sampleObject = preferences.getCustomValue<SampleObject>(
  'my-key',
  defaultValue: SampleObject.empty(),
  adapter: JsonAdapter(
    serializer: (value) => serializers.serialize(value),
    deserializer: (value) => serializers.deserialize(value),
  ),
);

The serializers here is your global serializer that comes from built_value.

streaming_shared_preferences's People

Contributors

denchikby avatar jonas-sander avatar jonaswanke avatar mapgoblin avatar roughike avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

streaming_shared_preferences's Issues

`MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences_macos)` when running tests

Description

I get MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences_macos) error when running the tests of this repository.

image

Steps to reproduce

git clone https://github.com/roughike/streaming_shared_preferences.git
cd streaming_shared_preferences
flutter test

Additional context

Flutter version: 2.10.4

Using with MultiProvider

in this simple code we can use provider:

Future<void> main() async {
  final preferences = await StreamingSharedPreferences.instance;
  final settings = MyAppSettings(preferences);

  runApp(
    Provider<MyAppSettings>.value(value: settings, child: MyApp()),
  );
}

and now my question is how can i implementing that with MultiProvider?

is this code correct?

  runApp(
    MultiProvider(providers: [
      Provider(builder: (_) => database.userTableDao),
      Provider(builder: (_) => database.postsTableDao),
    ], child: Provider<ApplicationSettings>.value(value: settings, child: OKToast(child: MyHomePage()))),
  );

Thanks in advance

Migrate example to Android embedding v2

Description

When running flutter pub get in the example, you will get this warning:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Warning
──────────────────────────────────────────────────────────────────────────────
Your Flutter application is created using an older version of the Android
embedding. It is being deprecated in favor of Android embedding v2. Follow the
steps at

https://flutter.dev/go/android-project-migration

to migrate your project. You may also pass the --ignore-deprecation flag to
ignore this check and continue with the deprecated v1 embedding. However,
the v1 Android embedding will be removed in future versions of Flutter.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
The detected reason was:

  /Users/nils/Desktop/Projects/streaming_shared_preferences/example/android/app/src/main/AndroidManifest.xml uses
  `android:name="io.flutter.app.FutterApplication"`
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Running "flutter pub get" in example...                          1,993ms

Extending PreferenceBuilder to multiple values

I like the PreferenceBuilder a lot. It would be even better if there would be a MultiPreferenceBuilder that allows to listen on multiple changing values, probably with Rx.combineLatest. It is a bit tedious to implement, as we would need to have it for 2, 3, 4, etc. Would it be possible to add such a feature?

ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.

This error occurs when running the example app:

Running Gradle task 'assembleDebug'...
✓ Built build/app/outputs/apk/debug/app-debug.apk.
Installing build/app/outputs/apk/app.apk...
E/flutter (18643): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
E/flutter (18643): If you're running an application and need to access the binary messenger before `runApp()` has been called (for example, during plugin initialization), then you need to explicitly call the `WidgetsFlutterBinding.ensureInitialized()` first.
E/flutter (18643): If you're running a test, you can call the `TestWidgetsFlutterBinding.ensureInitialized()` as the first line in your test's `main()` method to initialize the binding.
E/flutter (18643): #0      defaultBinaryMessenger.<anonymous closure> (package:flutter/src/services/binary_messenger.dart:76:7)
E/flutter (18643): #1      defaultBinaryMessenger (package:flutter/src/services/binary_messenger.dart:89:4)
E/flutter (18643): #2      MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:140:62)
E/flutter (18643): #3      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:146:35)
E/flutter (18643): #4      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
E/flutter (18643): #5      MethodChannel.invokeMapMethod (package:flutter/src/services/platform_channel.dart:356:48)
E/flutter (18643): #6      MethodChannelSharedPreferencesStore.getAll (package:shared_preferences_platform_interface/method_channel_shared_preferences.dart:54:22)
E/flutter (18643): #7      SharedPreferences._getSharedPreferencesMap (package:shared_preferences/shared_preferences.dart:187:57)
E/flutter (18643): #8      SharedPreferences.getInstance (package:shared_preferences/shared_preferences.dart:54:19)
E/flutter (18643): #9      debugObtainSharedPreferencesInstance (package:streaming_shared_preferences/src/streaming_shared_preferences.dart:300:23)
E/flutter (18643): #10     debugObtainSharedPreferencesInstance (package:streaming_shared_preferences/src/streaming_shared_preferences.dart:299:27)
E/flutter (18643): #11     StreamingSharedPreferences.instance (package:streaming_shared_preferences/src/streaming_shared_preferences.dart:40:7)
E/flutter (18643): #12     main (package:privacyidea_authenticator/testPref.dart:25:56)
E/flutter (18643): #13     _runMainZoned.<anonymous closure>.<anonymous closure> (dart:ui/hooks.dart:241:25)
E/flutter (18643): #14     _rootRun (dart:async/zone.dart:1184:13)
E/flutter (18643): #15     _CustomZone.run (dart:async/zone.dart:1077:19)
E/flutter (18643): #16     _runZoned (dart:async/zone.dart:1619:10)
E/flutter (18643): #17     runZonedGuarded (dart:async/zone.dart:1608:12)
E/flutter (18643): #18     _runMainZoned.<anonymous closure> (dart:ui/hooks.dart:233:5)
E/flutter (18643): #19     _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
E/flutter (18643): #20     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)

To prevent this, the main method should be changed to:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  /// Obtain instance to streaming shared preferences, create MyAppSettings, and
  /// once that's done, run the app.
  final preferences = await StreamingSharedPreferences.instance;
  final settings = MyAppSettings(preferences);

  runApp(MyApp(settings));
}

This should be mentioned in the documentation of this plugin also.

Question

Do I need to pass the setting from void main to streaming page?

containsKey method

Hello,
There is a containsKey method in shared preferences. Is the method not available for StreamingSharedPreferences?

Thank you

web中报错

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.0.0, on Microsoft Windows [Version 10.0.17134.984], locale zh-CN)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[√] Chrome - develop for the web
[√] Android Studio (version 4.1.0)
[√] VS Code (version 1.53.2)
[√] Connected device (2 available)
image

Why can't defaultValue be null?

Is there a reason why defaultValue can't be null?

Providing default values for all preferences is unintuitive ('' for a string instead of null) or for custom types can be quite difficult.

NULL Safety

Hello, I have recently migrated my project to Null Safety, and it this package is contradicting in migrating it. Kindly migrate it to null Safety.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.