Fling logo

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.

Build Status Code Quality Pub Version

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).