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

Overview

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.

Comments
  • Using with MultiProvider

    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

    opened by pishguy 3
  • Migrate to null-safety

    Migrate to null-safety

    I tried to keep the methods as close to the pre-null-safe-versions as possible, though maybe some of it should be changed with regards to #3.

    This migration includes a breaking change: As in the null-safe version of shared_preferences, setters no longer accept null to mean removing values. (remove(key) can be used instead.)

    opened by JonasWanke 2
  • ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.

    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.

    opened by timosturm 2
  • web中报错

    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

    opened by Spicely 1
  • adds ensureInitialized(); to readme and example

    adds ensureInitialized(); to readme and example

    Adds the changes to documentation and example to prevent the white screen of nothingness. #8

    https://stackoverflow.com/questions/57689492/flutter-unhandled-exception-servicesbinding-defaultbinarymessenger-was-accesse

    opened by mapgoblin 1
  • Update shared_preferences pubspec constraints

    Update shared_preferences pubspec constraints

    From here: https://pub.dev/packages/shared_preferences#backward-compatible-100-version-is-coming
    Backward compatible 1.0.0 version is coming.
    The plugin has reached a stable API, we guarantee that version 1.0.0 will be backward compatible with 0.5.y+z. Please use shared_preferences: '>=0.5.y+x <2.0.0' as your dependency constraint to allow a smoother ecosystem migration. For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0.

    opened by Jonas-Sander 1
  • Fix a bug where simultaneously listening to a reused Preference would not propagate changes to other listeners

    Fix a bug where simultaneously listening to a reused Preference would not propagate changes to other listeners

    As it turns out, the "initial stable release" was not bug-free.

    The bug

    If you did this:

    final counter = preferences.getInt('counter');
    
    int value1;
    int value2;
    int value3;
    
    counter.listen((value) => value1 = value);
    counter.listen((value) => value2 = value);
    counter.listen((value) => value3 = value);
    
    counter.setValue(9);
    

    Only value1 would now contain 9 as expected, while value2 and value3 would be 0.

    However, if you did:

    int value1;
    int value2;
    int value3;
    
    preferences.getInt('counter').listen((value) => value1 = value);
    preferences.getInt('counter').listen((value) => value2 = value);
    preferences.getInt('counter').listen((value) => value3 = value);
    
    counter.setValue(9);
    

    Everything was fine. The bug only appeared when reusing a Preference and when all the listeners were listening to it at the same time.

    To show the thing in action, see this screencast sharing a Preference between multiple PreferenceBuilders in a same screen:

    ezgif-1-63dfae1b07da

    The reason

    This bug was introduced by this commit. It happened because the cached T lastValue field should've existed in the onListen() callback, not as a field in the EmitValueChanges transformer.

    The purpose was to optimize the Preference stream so that the last emitted value wouldn't be unnecessarily added to the Stream again. Unfortunately, this "optimization" was too effective: it only allowed the first listener to receive the change but the other ones would always miss it.

    The fix

    The solution was to move the cached value inside the onListen() callback. This way each individual listener would enjoy the caching benefits while actually being able to receive updates.

    ezgif-1-7da864d4824e

    There is now a test case in place to prevent this from happening again.

    bug 
    opened by roughike 0
  • Please share a example for JSON interfacing

    Please share a example for JSON interfacing

    Hello @roughike, Please share a example for storing and retriving JSON object or any custom class. There is this adapter thing that i cant grasp the concept of. hjlabs.in

    opened by hemangjoshi37a 0
  • Migrate example to Android embedding v2

    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
    
    opened by nilsreichardt 0
  • `MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences_macos)` when running tests

    `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

    opened by nilsreichardt 2
  • Extending PreferenceBuilder to multiple values

    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?

    opened by degloff 2
  • Why can't defaultValue be null?

    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.

    opened by josh-burton 10
Releases(v1.0.2)
Owner
Iiro Krankka
Iiro Krankka
Simple reactive animations in your Flutter apps.

just.motion Flutter package to create organic motion transitions. Why? The Motion Value stateless hot reload status notifier Ease Motion Spring Motion

Roi Peker 49 Nov 14, 2022
A custom Flutter value slider that makes a wave effect when dragged.

A Flutter slider that makes a wave effect when dragged. Does a little bounce when dropped. Demo Getting Started To use this plugin, add wave_slider as

Gordon Hayes 33 Dec 21, 2022
SwiftUI - Examples projects using SwiftUI released by WWDC2019. Include Layout, UI, Animations, Gestures, Draw and Data.

SwiftUI Examples About Examples projects using SwiftUI & Combine. Include Layout, UI, Animations, Gestures, Draw and Data. See projects files in Files

Ivan Vorobei 4.2k Jan 8, 2023
Flutter tinder like cards

TCard Tinder like cards. TCard Install Usage Normal widget Network image Use a controller to control Determine the sliding direction Reset with new ca

Bruno Jurković 132 Dec 9, 2022
PageIndicator like Instagram written for Flutter

scrolling_page_indicator View page indicator like Instagram Getting Started Dependency dependencies: scrolling_page_indicator: ^0.1.2 Install flutte

null 30 Nov 4, 2022
A Flutter package to custom splash screen like change logo icon, logo animation, and splash screen background color.

Custom Splash Screen A Flutter package to custom splash screen: change logo icon, logo animation, and splash screen background color. (Custom from ani

tranhuycong 78 Sep 6, 2022
Flutter fluid slider - A fluid design slider that works just like the Slider material widget

Fluid Slider for Flutter Inspired by a dribbble by Virgil Pana. A fluid design s

Jay Raj 2 Feb 18, 2022
Widgets for creating Hero-like animations between two widgets within the same screen.

flutter_sidekick Widgets for creating Hero-like animations between two widgets within the same screen. Features Hero-like animations. Allow you to spe

Romain Rastel 291 Oct 21, 2022
🔔 A flutter package to create cool and beautiful text animations. [Flutter Favorite Package]

Animated Text Kit A flutter package which contains a collection of some cool and awesome text animations. Recommended package for text animations in C

Ayush Agarwal 1.4k Jan 6, 2023
This repository demonstrates use of various widgets in flutter and tricks to create beautiful UI elements in flutter for Android and IOS

AwesomeFlutterUI The purpose of this repository is to demonstrate the use of different widgets and tricks in flutter and how to use them in your proje

Subir Chakraborty 132 Nov 13, 2022
This is a Flutter URL preview plugin for Flutter that previews the content of a URL

flutter_link_preview This is a URL preview plugin that previews the content of a URL Language: English | 中文简体 Special feature Use multi-processing to

yung 67 Nov 2, 2022
Flutter liquid swipe - Liquid Swipe Animation Built With Flutter

Flutter Liquid Swipe liquid Swipe animation is amazing and its Created for iOS P

Jahongir Anvarov 6 Dec 1, 2022
A candy sorter game made with Flutter for the march flutter challenge.

A candy sorter game made with Flutter for the march flutter challenge.

Debjeet Das 1 Apr 9, 2022
✨ A collection of loading indicators animated with flutter. Heavily Inspired by http://tobiasahlin.com/spinkit.

✨ Flutter Spinkit A collection of loading indicators animated with flutter. Heavily inspired by @tobiasahlin's SpinKit. ?? Installing dependencies:

Jeremiah Ogbomo 2.7k Dec 30, 2022
A Flutter library for gradually painting SVG path objects on canvas (drawing line animation).

drawing_animation From static SVG assets See more examples in the showcasing app. Dynamically created from Path objects which are animated over time m

null 442 Dec 27, 2022
Flutter package for creating awesome animations.

?? Simple Animations Simple Animations is a powerful package to create beautiful custom animations in no time. ?? fully tested ?? well documented ?? e

Felix Blaschke 879 Dec 31, 2022
Fun canvas animations in Flutter based on time and math functions.

funvas Flutter package that allows creating canvas animations based on time and math (mostly trigonometric) functions. The name "funvas" is based on F

null 472 Jan 9, 2023
A flutter package that adds support for vector data based animations.

animated_vector Description and inspiration A package that adds support for vector data based animations. The data format is heavily inspired from the

Potato Open Sauce Project 6 Apr 26, 2022
Flutter UI Challenges

Introduction ?? Zoo is a small, simple and beautiful app that lists 3d model of animals. Before we start, you can take a look at the app: Usage ?? To

Sanjeev Madhav 58 Dec 22, 2022