streaming_shared_preferences 1.0.0 streaming_shared_preferences: ^1.0.0 copied to clipboard
A stream based wrapper over shared_preferences, allowing reactive key-value storage.
streaming_shared_preferences #
A reactive key-value store for Flutter projects.
It wraps shared_preferences with a reactive stream-based layer, allowing you to listen and react to changes in the underlying values. No external dependencies (such as rxdart) required!
Getting started #
First, add streaming_shared_preferences into your pubspec.yaml:
dependencies:
streaming_shared_preferences: ^1.0.0
If you're already using shared_preferences
, you should replace the dependency with streaming_shared_preferences
.
To get a hold of StreamingSharedPreferences
, await on instance
:
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
...
final preferences = await StreamingSharedPreferences.instance;
The public API follows the same naming convention as shared_preferences
does, but with a little
twist - every getter returns a Preference
object, which is a special type of Stream
.
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.
Quick overview of the API surface #
Here's plain Dart example on how you would print a value to console every time a counter
integer changes:
// Get a reference to the counter Preference, 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);
Assuming that there's no previously stored value for counter
, the above example will print 0
,
1
, 2
and 3
to the console.
We could also call preferences.setInt('counter', 1)
, but this is more convenient.
Since a Preference
knows how to set its own value, there's no need to provide a key.
Connecting it to Flutter widgets #
The recommended way is to use the PreferenceBuilder
widget.
If you have only one Preference
in your app, it might make sense to create and listen to a Preference
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, we 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 {
/// 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.
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 provided 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
.
"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);
}
class MyHomePageBloc {
MyHomePageBloc(this.settings);
final MyAppSettings 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 {
SettingsImplementation(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);
}