kind 0.5.1 kind: ^0.5.1 copied to clipboard
An unified data layer framework that enables serialization, persistence, and state observability for any Dart class. Comes with primitives such as decimal numbers and currency amounts.
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
- Decimal numbers
- Date and time
- Strings and bytes
- Lists and sets
- One of multiple values or kinds
- Money
- Geospatial
- Unique identifiers
- Arbitrary JSON trees
- Others
- Your custom models
- Recommended StringKind instances:
- stringKindForEmailAddress (email address)
- stringKindForMarkdown (Markdown formatted content)
- stringKindForPhoneNumber (phone number)
- stringKindForUrl (URL)
Other APIs #
- Currency (currency)
- CurrencyAmount (currency amount)
- Date (unlike DateTime, has only date)
- DateTimeWithTimeZone (unlike DateTime, allows arbitrary time zone)
- Decimal (decimal number)
- GeoPoint (geographical latitude/longitude coordinates)
- ReactiveIterable (reactive Iterable
- ReactiveList (reactive List
- ReactiveMap (reactive Map
- ReactiveSet (reactive Set
- UnitOfArea (unit of area)
- UnitOfLength (unit of length)
- UnitOfMeasure (unit of measure)
- UnitOfVolume (unit of volume)
- UnitOfWeight (unit of weight)
- 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.5.1
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 the string must have at least 1 character.
minLengthInUtf8: 1,
// 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,
),
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,
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 = JsonEncodingContext();
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;
}