A lightweight, small footprint Dart library for Serialization and Deserialization (SerDes). Forget initializing complex systems or annotating your classes - with Pickles, everything related to SerDes is visible in code and straightforward. Serialize your objects into JSON for readability or binary to optimize artifact size.
Usage
While Pickles are most frequently used to persist object state or structure, they can also be treated as heavyweight Memento objects - capable of performing undo/redo operations, for instance.
Compared to a classic memento, which does not allow classes other than the originator of the memento to create or "look inside" the memento itself, Pickles do provide a basic interface. The cost of this decision is that "counterfeit" mementos (created by something other than the class being serialized) might be passed off as originals by nefarious evildoers (or otherwise well-meaning developers working under a tight deadline). Further, since a Pickle must act as a generic state record, it is not as type-safe as a true memento. Consider these costs before replacing the traditional Memento pattern with Pickles in your classes.
Pickles do, however, provide some benefit as a lightweight serialization core for your non-sensitive classes. You can quickly set up a class whose state can be stored in a file or database to be restored at a later time without any messy initialization or Mirrors magic. This means better performance with a small footprint. If you later decide you need something more heavyweight, it is easy to extract - Pickles do not require any changes to, or conventions on, the classes themselves.
Pickles support serialization to and from binary and Dart's JSON representation. This means you can use them seamlessly
with a lot of the core libraries and add-ons, like dart:convert
or Firestore. See the examples below for details.
Examples
The simplest use case involves a class that wishes to save itself as a Pickle and be restored later. We'll work with a class that acts as a counter:
class Counter {
int count;
Counter([final int start = 0]) : count = start;
int countUp() => count++;
}
Full Integration
At a basic level, we only need to store the current count
in order to restore the object's state. We have several
options for making this class Pickle-friendly depending on how intrusive we want to be. We'll start with an example of
full integration:
class Counter implements Pickleable {
int count;
Counter([final int start = 0]) : count = start;
int countUp() => count++;
@override
Pickle toPickle() => PickleBuilder().withInt('count', count).build();
static Counter fromPickle(final Pickle pickle) => Counter(pickle.readInt('count'));
}
The three differences are:
- We implement the
Pickleable
interface - We implement
toPickle()
by producing a Pickle containing our object's state - We implement
fromPickle()
by producing a Counter based on a Pickle's values
With this, we have full integration with Pickles, and can do things like the following:
void main() {
var counter = Counter();
print(counter.countUp());
var pickle = counter.toPickle();
print(counter.countUp());
counter = Counter.fromPickle(pickle);
print(counter.countUp());
// Write our pickle to a file.
// We can use synchronous or asynchronous methods for this, as needed.
File('myCounter').writeAsBytesSync(Pickler().writeSync(pickle));
var pickleFromFile = Pickler().readSync(File('myCounter').readAsBytesSync());
counter = Counter.fromPickle(pickleFromFile);
print(counter.countUp());
}
// outputs: 0, 1, 1, 1
Furthermore, we have full support for nested Pickling. For example, a class that uses Counter
to produce version
numbers can easily "pickle" itself and the internal state of its Counter
:
class Version implements Pickleable {
final Counter _counter;
Version() : _counter = Counter();
Version._(final this.counter);
String nextVersion() => 'v${_counter.countUp()}';
@override
Pickle toPickle() => PickleBuilder().withPickleable('counter', _counter).build();
static Version fromPickle(final Pickle pickle) => Version._(pickle.readPickleable('counter'));
}
Serializing and deserializing a Version
is no different than that for a Counter
, including saving to a file or any
other means of persistence:
void main() {
var version = Version();
print(version.nextVersion());
var pickle = version.toPickle();
print(version.nextVersion());
version = Version.fromPickle(pickle);
print(version.nextVersion());
// save as a file
File('MyVersion.pickle').writeAsBytes(
BinaryPickler().writeSync(pickle));
// or to a database like Firestore
FirebaseFirestore.instance.collection('versions').add(
JsonPickler.writeSync(pickle));
}
// outputs: v0, v1, v1
Unintrusive Integration
If we do not want to (or cannot) modify the class we want to persist, we can still make use of Pickles as long as we
have access to the information we need to restore its state. Take the original Counter
class, for instance; we can
support basic "pickling" by writing a couple of methods outside of the class itself:
class Counter {
int count;
Counter([final int start = 0]) : count = start;
int countUp() => count++;
}
// ...Somewhere else...
Pickle counterToPickle(final Counter counter) => PickleBuilder().withInt('start', counter.count).build();
Counter counterFromPickle(final Pickle pickle) => Counter(pickle.readInt('start'));
With this, we won't have the nice integration support for nested Pickles, but we can still create Pickles and restore
a Counter
to a previous state:
void main() {
var counter = Counter();
print(counter.countUp());
var pickle = counterToPickle(counter);
print(counter.countUp());
counter = counterFromPickle(pickle);
print(counter.countUp());
}
And, of course, we can still implement a Version
class that makes use of these methods for nested Pickling:
class Version implements Pickleable {
final Counter _counter;
Version() : _counter = Counter();
Version._(final this.counter);
String nextVersion() => 'v${_counter.countUp()}';
@override
Pickle toPickle() => PickleBuilder().withPickle('counter', counterToPickle(_counter)).build();
static Version fromPickle(final Pickle pickle) => Version._(pickleToCounter(pickle.readPickle('counter')));
}
Choose the right level of integration for your project, and have fun!
Libraries
- fling_pickle
- Lightweight object Serialization and Deserialization (SerDes).