8 Dart Features Those Fat Cats Don’t Want You to Know

Productive code without ceremony

[contextly_sidebar id=”2bca5cb6af99cf17862f5497954ec380″]

In this article, I’ll show you eight more features that help Dart stand on its own as a productive, ceremony-free language. Remember, Dart compiles to JavaScript, so everything you see here works across the modern Web.

Dart makes fluent APIs easy

Libraries like jQuery have popularized a fluent design that encourages chaining calls for easier-to-read code. Dart takes a cue from Smalltalk and adds method cascades to the language, so that any API can be used in a fluent style.

Without cascades, the variable button is repeated for every method call.

// Without cascades.
var button = new ButtonElement();
button.id = 'awesome';
button.classes.add('important');
button.onClick.listen((e) => beAwesome());
button.text = 'Click Me!';

Use cascades to help reduce repetition.

// With cascades.
var button = new ButtonElement()
  ..id = 'awesome'
  ..classes.add('important')
  ..onClick.listen((e) => beAwesome())
  ..text = 'Click Me!';

All of the method calls that follow the .. are applied, one by one, to the object returned by new ButtonElement().

Cascades are also really useful when you want to create and initialize an object used as a method argument. Here is an example:

document.body.children.add(new ButtonElement()..id='awesome'..text='Click Me!';);

Thanks to method cascades, you don’t need to explicitly design your API to be fluent.

Dart has terse getters and setters

There’s no need to defensively and preemptively add getters and setters to your class. Dart lets you start with simple fields and evolve to explicit getters and setters without breaking existing users of your class.

Imagine a simple class for a car, with a single field for the engine’s status.

class Car {
  bool isEngineRunning;
}

main() {
  var roadster = new Car();
  print(roadster.isEngineRunning);
}

Later, an Engine class is added to Car. The Engine class has its own status field (isRunning), which makes Car’s isEngineRunning obsolete. We don’t want to change the API of Car, but we don’t want both an engine field and an isEngineRunning field. Luckily, we can add a getter which looks just like isEngineRunning but delegates to engine.

class Engine {
  bool isRunning;
}

class Car {
  Engine engine;

  bool get isEngineRunning => engine.isRunning;
}

main() {
  var roadster = new Car();
  print(roadster.isEngineRunning);
}

Notice how roadster.isEngineRunning looks the same in both examples. This means you no longer have to immediately create explicit getters and setters for every single field!

Dart has named constructors

Methods are great; you can name them pretty much whatever you want. However, the name of a constructor traditionally had to match the name of its class. Dart gives you named constructors for flexibility and clarity.

Consider a class that can be constructed from a JSON string or an XML string. Use named constructors to add clarity.

class Bunny {
  Bunny.fromXml(String xml) { … }
  Bunny.fromJson(String json) { … }
}

It’s now obvious how each new Bunny instance is created.

var fluffy = new Bunny.fromXml(xml);
var floppy = new Bunny.fromJson(json);

Dart replaces one-time callbacks with Futures

Futures replace callbacks as the preferred way to get a single result from an asynchronous operation. Futures can be chained and composed, and they are a fundamental building-block of the core Dart libraries.

Dart’s HttpRequest class (a cleaned up XMLHttpRequest) is a good example of how to use a Future.

import 'dart:html';

main() {
  HttpRequest.getString('/bunnies.json')       // request the resource
    .then((response) => handleJson(response))  // then, if successful,
                                               // handle the response
    .catchError((e) => recover(e));            // handle errors
}

You can easily chain Futures. Each then() callback runs when the previous Future completes.

  HttpRequest.getString('/bunnies.json')
    .then((response) => parse(response))    // returns a Future
    .then((object) => storeObject(object))  // returns a Future
    .catchError((e) => recover(e));

The catchError() call handles errors that occur along the entire chain.

Finally, because Dart supports first-class functions, you can further simplify the above code by simply passing function closures to then():

  HttpRequest.getString('/service.json')
    .then(parse)
    .then(storeObject)
    .catchError(recover);

Even though the above code is asynchronous, it reads like synchronous code.

Common callback-based APIs like requestAnimationFrame, getUserMedia, file I/O, and more now use Futures.

Dart replaces repeating callbacks with Streams

For one-shot responses, use Futures. For a continual stream of events, use Streams. Streams can be considered the “push” equivalent of iterables, and are useful for repeating events such as messages from a web socket, clicks from a button, or even new HTTP connections.

Like Futures, Streams are a core concept in the Dart libraries. Most APIs across the DOM and I/O now use Streams.

To get notified of an event, you can listen() to a Stream.

query('#button').onClick.listen((e) => handleClick());

You can chain and transform Streams. The following example code filters, transforms, and prints key presses via Streams.

query('textarea').onKeyPress
  .where((e) => e.keyCode >= 32 && e.keyCode <= 122)   .map((e) => new String.fromCharCode(e.charCode))
  .listen((char) => print('You typed $char'));

Streams have a rich API. You can cancel your subscription, take and skip events, pipe to another Stream, and more.

Dart has named optional parameters

You no longer have to guess what ambiguous parameters might mean, thanks to named parameters. To illustrate, consider this function call with two parameters. What does the last parameter mean?

connect('example.com', 4);  // what does 4 mean?

Use named parameters to add clarity for your APIs. Named parameters are optional, and they can have a default value. In the next example, retries has a default value of 3, which is used if the parameter is omitted when connect() is called.

void connect(String url, {int retries: 3}) { … }

// later:

connect('example.com', retries: 4);  // retries is 4
connect('example.com');              // retries is 3

Dart also supports positional optional parameters with default values.

Dart programs can be tree shaken

Check out the marketing material for most JavaScript libraries, and you’re certain to see a claim for how small the library is. Size is important, of course, but our tools should be trimming the size of our applications for us. I want to worry about how useful a library is, not about its size. Luckily, Dart has a solution.

Dart is a statically analyzable language, which means the structure of the program is known simply by parsing the code. No matter how many different libraries you import into your app, a “tree shaking” compiler can determine which classes and functions are actually used. The unused code is “shaken” out of the tree, and a single file is produced containing only the bits you need to run the app.

As an example, here’s a file that contains unused functionality:

// not used in this program!
flySpaceship() {
  print('Strap in!');
}

knitSweaters() {
  print("It's like wearing a hug");
}

main() {
  knitSweaters();
}

Use dart2js to generate a tree-shaken file and eliminate unused code:

knitSweaters() { print("It's like wearing a hug"); }
main() { knitSweaters(); }

Tree shaking works when compiling Dart to JavaScript, or simply converting your Dart app to a single deployable Dart file.

Dart replaces shared-memory threads with isolates

Concurrency is great, but shared-memory threads are error prone. Dart implements an isolate system for safer concurrent programming. Isolates are “isolated memory heaps” that can be spawned from a top-level function or URI. Isolates communicate by passing messages, which are copied before they are sent.

You can use isolates for many things, including running a long process in the background. Here is an example:

import 'dart:isolate';

longRunningCalculation() {
  port.receive((msg, replyTo) {
    if (msg == 'go') {
      // do long calculation
      replyTo.send(result);
    }
  });
}

main() {
  SendPort sendPort = spawnFunction(longRunningCalculation);
  sendPort.call('go').then((result) => print('Result is $result'));

  // continue to respond to input events while long calculation is running
}

Isolates compile to Web workers if running via dart2js. We expect the isolate API to adopt Futures and Streams.

Summary

The Dart language and core libraries are productive and easy to learn, with features for asynchronous programming, concurrency, eliminating dead code, and more. You can learn more about Dart in Dart: Up & Running, and learn Dart web programming with our Dart tutorials. Find our downloads, community, and ways to get involved in our open-source project at dartlang.org.

tags: , , , ,