kind 0.5.2
kind: ^0.5.2 copied to clipboard

A serialization / reflection framework. Supports JSON and Protobuf / GRPC. Comes with primitives such as Currency and Uuid.

Pub Package Github Actions CI

Overview #

An unified data layer framework that enables serialization, persistence, and state management for any Dart class. This an early version and the APIs are not frozen. The package requires a null-safe Flutter SDK 2.0.0 or later.

Features #

  • Encode/decode JSON.
    • The package can handle most JSON serialization requirements.
  • Encode/decode Protocol Buffers.
    • The package can handle most Protocol Buffers (and GRPC) serialization requirements.
  • Use databases (upcoming).
    • Our sibling package database will use this framework in future.
  • Observe views / mutations in graphs
    • The package has ReactiveSystem for observing views and mutations of reactive states in the isolate.
    • When you are deserializing JSON or Protocol Buffers, you get reactive objects by default.

Overview of APIs #

Built-in kinds #

Other APIs #

Some alternatives #

Getting started #

1.Adding dependency #

In pubspec.yaml, you should have something like:

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  kind: ^0.5.2

2.Write data models #

When you extend Entity and declare EntityKind for your entity, you get:

  • == (that handles cycles correctly)
  • hashCode
  • toString()
  • JSON serialization
  • Protocol Buffers serialization
  • And more!

There are many ways to declare EntityKind. Your choice depends on:

  • Is your class immutable or mutable?
  • Do you need state observation?

Immutable class? #

import 'package:kind/kind.dart';

class Pet extends Object with EntityMixin {
  static final EntityKind<Pet> kind = EntityKind<Pet>(
    name: 'Pet',
    define: (c) {
      // In this function, we define:
      //   * Properties
      //   * Constructor
      //   * Additional metadata (examples, etc.)

      // Property #1
      //
      // Here "optionalString()" means "define a nullable string property".
      // It returns an instance of Prop<Pet, String?>.
      final nameProp = c.optionalString(
        // ID is some unique integer that's 1 or greater.
        //
        // It's used by (optional) Protocol Buffers serialization.
        //
        id: 1,

        // Name of the field.
        //
        // It's used by, for example, JSON serialization.
        //
        name: 'name',

        // Here we say that a valid string should:
        //   * Have at least 1 character.
        //   * Have no more than 80 characters.
        minLengthInUtf8: 1,
        maxLengthInUTf8: 80,

        // Getter that returns value of the field.
        getter: (t) => t.name,
      );

      // Property #2
      final bestFriendProp = c.optional<Pet>(
        id: 2,
        name: 'bestFriend',
        kind: Pet.kind,
        getter: (t) => t.bestFriend,
      );

      // Property #3
      final friendsProp = c.requiredList<Pet>(
        id: 3,
        name: 'friends',
        itemsKind: Pet.kind,
        getter: (t) => t.friends,
      );

      // Example (optional)
      c.examples = [
        Pet(name: 'Charlie'),
      ];

      // Define constructor
      c.constructorFromData = (data) {
        return Pet(
          name: data.get(nameProp),
          bestFriend: data.get(bestFriendProp),
          friends: data.get(friendsProp),
        );
      };
    },
  );

  /// Full name.
  final String? name;

  /// Best friend.
  final Pet? bestFriend;

  /// List of friends.
  final List<Pet> friends;

  Pet({
    this.name,
    this.bestFriend,
    this.friends = const [],
  });

  @override
  EntityKind<Pet> getKind() => kind;
}

Property definition methods such as optionalString (in EntityKindDefineContext) are just convenient methods for adding Prop<T,V> instances. They save a few lines compared to something like:

c.addProp(Prop<Pet, String?>(
  id: 1,
  name: 'name',
  kind: const StringKind(
    minLengthInUtf8: 1,
    maxLengthInUTf8: 80,
  ),
  getter: (t) => t.name,
));

Mutable class? #

import 'package:kind/kind.dart';

class Pet extends Object with EntityMixin {
  static final EntityKind<Pet> kind = EntityKind<Pet>(
    name: 'Pet',
    define: (c) {
      // Property #1
      c.optionalString(
        id: 1,
        name: 'name',
        minLengthInUtf8: 1,
        maxLengthInUTf8: 80,
        getter: (t) => t.name,
        setter: (e,v) => e.name = v, // <-- Not defined in the earlier approach.
      );

      // Property #2
      c.optional<Pet>(
        id: 2,
        name: 'bestFriend',
        kind: Pet.kind,
        getter: (t) => t.bestFriend,
        setter: (e,v) => e.bestFriend = v, // <-- Not defined in the earlier approach.
      );

      // Property #3
      c.requiredList<Pet>(
        id: 3,
        name: 'friends',
        itemsKind: Pet.kind,
        getter: (t) => t.friends,
        setter: (e,v) => e.friends = v, // <-- Not defined in the earlier approach.
      );

      // Define constructor
      c.constructor = () => Pet(); // <-- Different from the earlier approach.
    },
  );

  /// Full name.
  String? name;

  /// Best friend.
  Pet? bestFriend;

  /// List of friends.
  List<Pet> friends = [];

  @override
  EntityKind getKind() => kind;
}

JSON serialization #

Use JsonEncodingContext and JsonDecodingContext:

import 'package:kind/kind.dart';

void main() {}
  final cat = Pet();
  bob.name.value = 'Bob';
  
  final dog = Pet();
  alice.name.value = 'Alice';
  alice.bestFriend = cat;

  // Pet --> JSON tree
  final encodingContext = JsonEncodingContext();
  final dogJson = encodingContext.encode(dog, kind: Pet.kind);
  // JSON tree:
  //   {
  //     "name": "Alice",
  //     "bestFriend": {
  //       "name": "Bob",
  //     }
  //   }
  
  // JSON tree --> Pet
  final decodingContext = JsonDecodingContext();
  final decodedDog = decodingContext.decode(dogJson, kind: Pet.kind);
}

If you want to map identifiers to different naming convention or have special identifier rules, use Namer.

If you want to change entirely how some classes are serialized, override methods in JsonEncodingContext and JsonDecodingContext.

Protocol Buffers serialization #

Use ProtobufEncodingContext and ProtobufDecodingContext:

// Pet --> bytes
final encodingContext = ProtobufEncodingContext();
final bytes = encodingContext.encodeBytes(pet, kind: Pet.kind);

// bytes --> Pet
final decodingContext = ProtobufDecodingContext();
final pet = decodingContext.decodeBytes(bytes);

For encoding/decoding GeneratedMessage (used by package:protobuf and package:grpc), use encode(...) and decode(...):

// Pet --> GeneratedMessage
final generatedMessage = encodingContext.encode(pet);

// GeneratedMessage --> Pet
final pet = decodingContext.decode(generatedMessage);

You can also generate separate GeneratedMessage classes with GRPC tooling and merge messages (using mergeFromMessage and other methods available).

If you want to map identifiers to different naming convention or have special identifier rules, use Namer.

If you want to change entirely how some classes are serialized, override methods in ProtobufEncodingContext and ProtobufDecodingContext.

Defining reactive classes #

Approach #1: Field

In the following example, we use Field and ListField. They handle sending of notifications to ReactiveSystem.

import 'package:kind/kind.dart';

class Pet extends Object with EntityMixin {
  static final EntityKind<Pet> kind = EntityKind<Pet>(
    name: 'Pet',
    define: (c) {
      // Property #1
      c.optionalString(
        id: 1,
        name: 'name',
        minLengthInUtf8: 1,
        field: (e) => e.name,
      );

      // Property #2
      c.optional<Pet>(
        id: 2,
        name: 'bestFriend',
        kind: Pet.kind,
        field: (e) => e.bestFriend,
      );

      // Property #3
      c.requiredList<Pet>(
        id: 3,
        name: 'friends',
        itemsKind: Pet.kind,
        field: (e) => e.friends,
      );

      // Define constructor
      c.constructor = () => Pet();
    },
  );

  /// Full name.
  late final Field<String?> name = Field<String?>(this);

  /// Best friend.
  late final Field<Pet?> friends = Field<Pet?>(this);

  /// List of friends.
  late final ListField<Pet> friends = ListField<Pet>(this);

  @override
  EntityKind getKind() => kind;
}

Approach #2: ReactiveMixin #

You can use ReactiveMixin for implementing getters and setters that send notifications to ReactiveSystem.

This is a bit more error-prone approach than the approach above.

import 'package:kind/kind.dart';

class Pet extends Entity with ReactiveMixin {
  static final EntityKind<Pet> kind = EntityKind<Pet>(
    name: 'Pet',
    define: (c) {
      // Property #1
      c.optionalString(
        id: 1,
        name: 'name',
        minLengthInUtf8: 1,
        getter: (t) => t.name,
        setter: (e,v) => e.name = v, // <-- Not defined in the earlier approach.
      );

      // Property #2
      c.optional<Pet>(
        id: 2,
        name: 'bestFriend',
        kind: Pet.kind,
        getter: (t) => t.bestFriend,
        setter: (e,v) => e.bestFriend = v, // <-- Not defined in the earlier approach.
      );

      // Property #3
      c.requiredList<Pet>(
        id: 3,
        name: 'friends',
        itemsKind: Pet.kind,
        getter: (t) => t.friends,
        setter: (e,v) => e.friends = v, // <-- Not defined in the earlier approach.
      );

      // Define constructor
      c.constructor = () => Pet(); // <-- Different from the earlier approach.
    },
  );

  String? _name;
  Pet? _bestFriend;
  List<Pet> _friends = ReactiveList<Pet>.empty();

  /// Full name.
  String? get name => beforeGet(_name);
  set name(String? value) => _name = beforeSet(_name, value);

  /// Best friend.
  Pet? get bestFriend => beforeGet(_bestFriend);
  set bestFriend(Pet? value) => _bestFriend = beforeSet(_bestFriend, value);

  /// List of friends.
  List<Pet> get friends => beforeGet(_friends);
  set friends(List<Pet> value) => _friends = beforeSet(_friends, value);

  @override
  EntityKind<Pet> getKind() => petKind;
}
4
likes
120
pub points
87%
popularity

Publisher

dint.dev

A serialization / reflection framework. Supports JSON and Protobuf / GRPC. Comes with primitives such as Currency and Uuid.

Repository (GitHub)
View/report issues

Documentation

API reference

License

Apache 2.0 (LICENSE)

Dependencies

collection, fixnum, meta, protobuf

More

Packages that depend on kind