kind 0.4.0 kind: ^0.4.0 copied to clipboard
An unified data layer framework that enables serialization, persistence, and state observability for any Dart class. Code generation is not necessary.
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.
Links #
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 #
- Booleans
- Integers
- Floating-point numbers
- Date and time
- Strings and bytes
- Lists and sets
- Others
- Your custom models
- Recommended StringKind instances:
- stringKindForEmailAddress (email address)
- stringKindForMarkdown (Markdown formatted content)
- stringKindForPhoneNumber (phone number)
- stringKindForUrl (URL)
Other APIs #
- Date (unlike DateTime, has only date)
- DateTimeWithTimeZone (unlike DateTime, allows arbitrary time zone)
- GeoPoint (geographical latitude/longitude coordinates)
- ReactiveIterable
- ReactiveList
- ReactiveMap
- ReactiveSet
- Uuid (128-bit object identifier)
Some alternatives #
- For serialization
- For state management
Getting started #
1.Adding dependency #
In pubspec.yaml, you should have something like:
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
kind: ^0.3.2
2.Write data models #
In the following example, we use Field and ListField. Wrapping values inside Field simplifies state observation. If you want to use normal Dart getters / setters, see "Alternative approaches" section below.
import 'package:kind/kind.dart';
class Person extends Entity {
static final EntityKind<Person> kind = EntityKind<Person>(
name: 'Person',
build: (b) {
b.optionalString(
id: 1,
name: 'fullName',
minLength: 1,
field: (e) => e.fullName,
);
b.requiredList<Person>(
id: 2,
name: 'friends',
itemsKind: Person.kind,
field: (e) => e.friends,
);
b.constructor = () => Person();
},
);
/// Full name.
late final Field<String?> fullName = Field<String?>(this);
/// Friends.
late final ListField<Person> friends = ListField<Person>(this);
@override
EntityKind getKind() => kind;
}
void main() {
final alice = Person();
alice.name.value = 'Alice';
final bob = Person();
bob.name.value = 'Bob';
alice.friends.add(bob);
bob.friends.add(alice);
// Your objects have:
// * `==` (that supports cyclic graphs)
// * `hashCode`
// * `toString()``
// * And more!
}
JSON serialization #
Use jsonTreeEncode(...) and jsonTreeDecode(...):
final alice = Person();
final bob = Person();
alice.name.value = 'Alice';
bob.name.value = 'Bob';
alice.friends.add(bob);
// Person --> JSON tree
final aliceJson = person.getKind().jsonTreeEncode(alice);
// JSON tree:
// {
// "fullName": "Alice",
// "friends": [
// {
// "fullName": "Bob",
// }
// ]
// }
// JSON tree --> Person
final decodedAlice = Person.kind.jsonTreeDecode(aliceJson);
Mapping identifiers #
If you want to use underscore naming convention, simply pass UnderscoreNamer in the context object:
final namer = UnderscoreNamer();
// Person --> JSON tree
final aliceJson = person.getKind().jsonTreeEncode(
alice,
context: JsonEncodingContext(namer: namer),
);
// JSON tree --> Person
final decodedAlice = Person.kind.jsonTreeDecode(
aliceJson,
context: JsonDecodingContext(namer: namer),
);
You can also declare special rules:
final namer = UnderscoreNamer(
rules: {
'fullName': 'real_name',
},
);
Protocol Buffers serialization #
For encoding/decoding Protocol Buffers bytes, use protobufBytesEncode(...) and protobufBytesDecode(...):
// Person --> bytes
final bytes = Person.kind.protobufBytesEncode(person);
// bytes --> Person
final person = Person.kind.protobufBytesDecode(bytes);
For encoding/decoding GeneratedMessage (used by package:protobuf and package:grpc), use protobufTreeEncode(...) and protobufTreeDecode(...):
// Person --> GeneratedMessage
final generatedMessage = Person.kind.protobufTreeEncode(person);
// GeneratedMessage --> Person
final person = Person.kind.protobufTreeDecode(generatedMessage);
You can also generate GeneratedMessage classes with GRPC tooling and merge messages using mergeFromMessage.
Alternative approaches to specifying data classes #
Why / why not? #
The alternative approaches:
- Do not force you to deviate from the way you normally write classes.
- Perform better when you have millions of objects.
- Do not support reactive programming with
ReactiveSystem
unless you write a lot error-prone boilerplate code.- At some point, we may release a code generator that generates boilerplate for you, but there will inevitably going to be some complexity unless Dart language designers decide to support something like decorator annotations.
Mutable and non-reactive #
You just define getter
and setter
in Prop
for ordinary Dart fields:
import 'package:kind/kind.dart';
class Person {
/// Full name.
String? fullName = '';
/// Friends.
final Set<Person> friends = {};
}
/// EntityKind for [Person].
final EntityKind<Person> personKind = EntityKind<Person>(
name: 'Person',
build: (builder) {
builder.optionalString(
id: 1,
name: 'fullName',
getter: (t) => t.fullName,
setter: (t,v) => t.fullName = v,
);
builder.requiredSet<Person>(
id: 2,
name: 'friends',
itemsKind: personKind,
getter: (t) => t.friends,
);
builder.constructor = () => Person();
},
);
Mutable and reactive #
You can use ReactiveMixin for implementing getters and setters that send notifications to ReactiveSystem:
// ...
class Person extends Entity with ReactiveMixin {
String? _fullName;
final Set<Person> _friends = ReactiveSet<Person>();
/// Full name of the person.
String? get fullName => beforeGet(_fullName);
set fullName(String? value) => _fullName = beforeSet(_fullName, value);
/// Friends of the person.
Set<Person> get friends => beforeGet(_friends);
@override
EntityKind<Person> getKind() => personKind;
}
// `personKind` is identical to the previous example.
// ...
Immutable and non-reactive #
import 'package:kind/kind.dart';
// Extending Entity is optional, but recommended.
class Person {
/// Full name of the person.
final String? name;
/// Friends of the person.
final Set<Person> friends;
Person({
this.fullName,
this.friends = const {},
});
}
/// EntityKind for [Person].
final EntityKind<Person> personKind = EntityKind<Person>(
name: 'Person',
build: (builder) {
final fullName = builder.optionalString(
id: 1,
name: 'fullName',
getter: (t) => t.fullName,
);
final friends = builder.requiredSet<Person>(
id: 2,
name: 'friends',
itemsKind: personKind,
getter: (t) => t.friends,
);
builder.constructorFromData = (data) {
return Person(
name: data.get(fullName),
friends: data.get(friends),
);
};
},
);
Immutable and reactive #
You can use ReactiveMixin for implementing getters and setters that send notifications to ReactiveSystem:
// ...
// Extending Entity is optional, but recommended.
class Person extends Entity with ReactiveMixin {
final String? _fullName;
final Set<Person> _friends;
/// Full name of the person.
String? get fullName => beforeGet(_fullName);
/// Friends of the person.
Set<Person> get friends => beforeGet(_friends);
Person({
required String? name,
Set<Person> friends = const {},
}) :
_fullName = name,
_friends = ReactiveSet<Person>.wrap(friends);
@override
EntityKind<Person> getKind() => personKind;
}
// `personKind` is identical to the previous example.
// ...