Recently I’ve been exploring dart appengine and package:gcluod for Universal Dart. There has not yet been enough documents for them so I often refer to their library implementation and pub.dartlang.org implementation, and also the cloud datastore documentation for other languages.

Here I will occasionally write my experiments and development log. This is the first one to share.

Sharing code between client and server with Dart.

With the dart appengine experiment, I’m trying to share some parts of code between the client and server in my production. You might hear about the terms “Isomorphic JavaScript” or “Universal JavaScript”, in which ways many parts of the code are shared between client and server, and they even have a chance to enable server side rendering (SSR). They have both benefits and difficulties in its own right. Someday I find the best practice of code sharing with Dart. But in this phase, I try to share some small parts which I think are obviously beneficial.

Entity Interface.

Entity is an object with data inserted into and retrieved from a server side database(mostly). And the data is serialized and deserialized for communicating with a client over a network.

Given this entity interface Foo placed in package:client_server, for sharing via the package manager.

// At package:client_server/entity.dart

/// Interface Foo implements Timestamps, and having one concrete implementation.
abstract class Foo implements Timestamps {
/// Datastore id (Type: String).
String get id;
bool get isSomething;
List<String> get aList;
// A domain specific name alias to id.
String get text => id;

// some validation method here sounds nice.
}

/// Timestamps interface.
/// CreatedTime and UpdatedTime interfaces are separated. Because there can be an entity which don't need updatedTime interface.
abstract class Timestamps = CreatedTime with UpdatedTime;

/// For an Entity which don't need updatedTime interface.
abstract class CreatedTime {
DateTime get createdTime;
}

abstract class UpdatedTime {
DateTime get updatedTime;
}

Server Side.

Then Foo should implement entity.Foo interface which should implement Timestamps interface.

/// Server side.

import 'package:gcloud/db.dart' as db;
import 'package:client_server/entity.dart' as entity;

@db.Kind(idType: db.IdType.String)
class Foo extends db.Model with entity.Foo {
@override
@db.BoolProperty(required: true)
bool isSomething = false;
@override
@db.ListProperty(const db.StringProperty(), indexed: false)
List<String> aList = [];
@override
@db.DateTimeProperty(required: true)
DateTime createdTime;
@override
@db.DateTimeProperty(required: true)
DateTime updatedTime;

// Some server side specific constructors below...

// Some server side specific methods below...
}

In above example, notice Foo inherits entity.Foo via db.Model with mixin, rather than extends or implements.

Because I want Foo to inherit entity.Foo's concrete methods (in this example, String get text => id;) which is expected to work both on client and server, instead of implementing again and adding @override annotation since they are tedious duplicate work. On the other hand, a datastore model must "extends" db.Model by design. (If Foo inherits entitiy.Foo with db.Model, it's statically no error but it causes a runtime error on its initialization process using mirror(reflection) internally).

So I mixin entity.Foo to db.Model to be extended by Foo sub class. Dart static analyzer still let Foo to implement missing concrete implementation of entity.Foo's interfaces. In this example, getters of createdTime, updatedTime, isSomething, aList. (id is implemented by db.Model).

class Foo extends db.Model with entity.Foo

Client Side.

The client side implementation is straightforward.

/// Client side.

import 'package:client_server/entity.dart' as entity;

class Foo extends entity.Foo {
@override
final String id;
@override
bool isSomething;
@override
List<String> aList;
@override
final DateTime createdTime;
@override
final DateTime updatedTime;

Foo(this.id, this.isSomething, this.aList, this.createdTime,
this.updatedTime);

// Some other client side specific constructors below...

// Some client side specific methods below...
}

The client side Foo class extends the same abstract class entity.Foo, which derives same interface to Foo, that is, entity.Foo let Foo to implements missing concrete implementations and derives one concrete implementation.

On the client side, I add final to id, createdTime, and updatedTime because they are never edited on the client side. As you know, a field with final keyword exposes only its implicit getter method, and the implicit setter is not created. I define one constructor for the final constraint.

One might have an idea that entity.Foo can become the non abstract class, having fields and constructors instead of getters. And just use it as entity on both client side and server side. Defining separated Foo extends db.Model implements entity.Foo just for the database schema and converting data back and forth between them and a entity.Foo object?

I tried it, but I found it could introduce various issues. For instance, in some cases I wanted to add final to some fields only on the client side, also needed field overrides and a constructor only for client side. There was methods working only on the client side. So after all I created the sub class only on the client side. it was not so simpler than the first example above. Furthermore, I encountered invalid override issue on the server side (db.Model has Object id field, and implementing String id rule from entity.Foo conflicts). So I had to relax the id field's type from String to Object, which was regrettable (It could be better if package:gcloud db.Model introduced sub classes for both String and int id field). Using generics could also introduce some covariant issues. Having both entity sub class on server and db.Model sub class and passing data each other increases some complexity.

It could work very well on some use cases, although in practice I still only share the minimum interfaces with caution. I really hope to find a better practice.

Serializer / Deserializer

Sharing entity data over a network requires data serialization / deserialization. Sharing the converting methods sounds good.

The easiest but tedious and error-prone implementation is to write serialize() and deserialize() function for every entity by hand. For example, this is similar to what I'm implementing in my production.

/// A server side serialization.
String serialize(Foo foo) {
return JSON.encode({
'id': foo.id,
'isSomething': foo.isSomething,
'aList': foo.aList,
// serialize utc DateTime to iso8601String.
'createdTime': foo.createdTime.toIso8601String(),
'updatedTime': foo.updatedTime.toIso8601String()
});
}

/// A server side deserialization.
/// Then validate the fields and update to save into datastore.
Foo deserialize(Strnig serializedData) {
Map data = JSON.decode(serializedData);
return new Foo(data['id'], data['isSomething'], data['aList']);
}

/// A client side deserialization.
Foo deserialize(String serializedData) {
Map data = JSON.decode(serializedData);
return new Foo(data['id'], data['isSomething'], data['aList'],
// deserialize utc iso8601String to local DateTime object.
DateTime.parse(data['createdTime']).toLocal(),
DateTime.parse(data['updatedTime']).toLocal());
}
/// A client side serialization.
/// The fields of createdTime and updatedTime is not necessary when they are managed exclusively in server side.
String serialize(Foo foo) {
return JSON.encode({
'id': foo.id,
'isSomething': foo.isSomething,
'aList': foo.aList
});
}
  • In my actual product code, the JSON encode/decode process are extracted out.

The requirements may vary where the entity lives. (Or, you don’t care?)

Using could dart:mirror mitigate the pain, however, There are the serious problem of dart:mirror on the client side. For instance, code size will increase on the client side web app's compilation to JavaScript. Flutter disables dart:mirror.

There’s the page for summarizing and discussing the serialization problem.

In any case, writing each converting code with one language itself is definitely a huge improvement.

Tip: The package installation.

On both package:client and package:server, install package:client_server with “path” option, so that you don't need to pub get on every code change of package:client_server.

dependencies:
client_server:
path: ../client_server

Writing both client and server code with same language brings huge improvement of productivity, because of no mental context switch of languages, powerful and fast static analyzer with sound typing, and utilizing core libraries such as Future, Stream, Collection, Uri, and common universal libraries such as package:async, package:http, package:quiver, package:test, etc...

While the ecosystem is still early stage and many issues exist, I feel happy to write whole production code with Dart.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.