Idiomatic Dart
Written by Bob Nystrom
October 2011
(updated March 2013)
Dart was designed to look and feel familiar if you’re coming from other
languages, in particular Java and JavaScript. If you try hard enough, you can
use Dart just like it was one of those languages. If you try really hard, you
may even be able to turn it into Fortran, but you’ll be missing out on what’s
unique and fun about Dart.
This article will help teach you to write code that’s uniquely suited for Dart.
Since the language is still evolving, many of the idioms here are changing too.
There are places in the language where we still aren’t sure what the best
practice is yet. (Maybe you can help us.) But here are some pointers that will
hopefully kick your brain out of Java or JavaScript mode, and into Dart.
Contents
- Constructors
- Automatic field initialization
- Named constructors
- Factory constructors
- Functions
- One-line methods
- Function types and aliases
- Type annotations
- Fields, getters, and setters
- Top-level definitions
- Dependency injection
- Strings and interpolation
- Operators
- Equality
- Numbers
- Futures
- Comments
Constructors
We’ll start this article the way every object starts its life: with
constructors. Every object will be constructed at some point, and defining
constructors is an important part of making a usable class. Dart has a few
interesting ideas here.
Automatic field initialization
First up is getting rid of some tedium. Many constructors simply take their
arguments and assign them to fields, like:
class Point {
num x, y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
So we’ve got to type x four times here just to initialize a field. Lame. We
can do better:
class Point {
num x, y;
Point(this.x, this.y);
}
If an argument has this. before it in a constructor argument list, the field
with that name will automatically be initialized with that argument’s value.
This example shows another little feature too: if a constructor body is
completely empty, you can just use a semicolon (;) instead of {}.
Named constructors
Like most dynamically-typed languages, Dart doesn’t support overloading. With
methods, this isn’t much of a limitation because you can always use a different
name, but constructors aren’t so lucky. To alleviate that, Dart lets you define
named constructors:
import 'dart:math';
class Point {
num x, y;
Point(this.x, this.y);
Point.zero() : x = 0, y = 0;
Point.polar(num theta, num radius) {
x = cos(theta) * radius;
y = sin(theta) * radius;
}
}
Here our Point class has three constructors, a normal one and two named ones.
You can use them like so:
import 'dart:math';
main() {
var a = new Point(1, 2);
var b = new Point.zero();
var c = new Point.polar(PI, 4.0);
}
Note that we’re still using new here when we invoke the named constructor. It
isn’t just a static method.
Factory constructors
There are a couple of design patterns floating around related to factories.
They come into play when you need an instance of some class, but you want to be
a little more flexible than just hard-coding a call to a constructor for some
concrete type. Maybe you want to return a previously cached instance if you have
one, or maybe you want to return an object of a different type.
Dart supports that without requiring you to change what it looks like when you
create the object. Instead, you can define a factory constructor. When you
call it, it looks like a regular constructor. But the implementation is free
to do anything it wants. For example:
class Symbol {
final String name;
static Map<String, Symbol> _cache;
factory Symbol(String name) {
if (_cache == null) {
_cache = {};
}
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final symbol = new Symbol._internal(name);
_cache[name] = symbol;
return symbol;
}
}
Symbol._internal(this.name);
}
Here we have a class that defines symbols. A symbol is like a string but we
guarantee that there will only be one symbol with a given name in existence at
any point in time. This lets you safely compare two symbols for equality just by
testing that they’re the same object.
The default (unnamed) constructor here is prefixed with factory. That tells
Dart that this is a factory constructor. When it’s invoked, it will not create
a new object. (There is no this inside a factory constructor.) Instead, you
are expected to create an instance and explicitly return it. Here, we look for a
previously cached symbol with the given name and reuse it if we found it.
What’s cool is that the caller never sees this. They just do:
var a = new Symbol('something');
var b = new Symbol('something');
assert(identical(a, b)); // true!
The second call to new will return the previously cached object. This is nice
because it means that if we don’t need a factory constructor at first, but later
realize we do, we won’t have to change all of our existing code that’s calling
new to instead call some static method.
Functions
Like most modern languages, Dart features first-class functions with full
closures and a lightweight syntax. Functions are objects just like any other,
and you shouldn’t hesitate to use them freely.
Dart has three notations for creating functions:
- named functions
- anonymous functions with statement bodies
- expression bodies, also known as arrow functions
The named form looks like this:
bool isShouting(String message) {
return (message.toUpperCase() == message);
}
The above example looks similar to functions or methods
found in C or Java. You can call functions in the usual way:
print(isShouting("I'M JUST VERY EXCITED")); // true
In Dart, functions are objects, so you can pass them as arguments:
var messages = ['hello', 'DART IS FUN'];
var shouts = messages.where(isShouting).toList();
print(shouts); // ['DART IS FUN']
If you don’t need to give a name to a function, there is an anonymous form, too.
It looks like a named function, but without a name or return type.
Here is an example:
var shouts2 = messages.where((m) {
return (m.toUpperCase() == m);
}).toList();
Finally, if you need a really lightweight function that just evaluates
and returns a single expression, there’s =>:
var shouts3 = messages.where((m) => m.toUpperCase() == m).toList();
A parenthesized argument list followed by => and a single expression
creates a function that takes those arguments, and returns the result of the
expression.
In practice, we find ourselves preferring arrow functions whenever possible
since they’re terse but still easy to spot thanks to =>. We use anonymous
functions frequently for event handlers and callbacks.
Read more about
functions in Dart.
One-line methods
Dart has one more trick up its sleeve that is one of my favorite features of the
language: you can also use => for defining members. Sure, you could do
this:
class Rectangle {
num width, height;
bool contains(num x, num y) {
return (x < width) && (y < height);
}
num area() {
return width * height;
}
}
But why do that when you can just do:
class Rectangle {
num width, height;
bool contains(num x, num y) => (x < width) && (y < height);
num area() => width * height;
}
We find arrow functions are great for defining simple getters and other one-
liner methods that calculate or access some property of an object.
Function types and aliases
As a reminder, Dart allows you to pass a function as an argument to another
function. Here is an example:
List<num> filterNumbers(List<num> numbers, bool filter(num x)) {
return numbers.where(filter).toList();
}
While the above code works, it would be nice to extract extra type information
about filter. As it is written, you can’t ask what kind of function
filter is. That is, you can’t say if (filter is bool filter(num x)).
Also, the syntax is a bit noisy in the signature for filterNumbers.
To help clean up function signatures, and to provide a bit more type
information about functions, you can use a typedef.
A typedef essentially provides an alias for a function signature.
typedef bool Filter(num x);
List<num> filterNumbers(List<num> numbers, Filter filter) {
return numbers.where(filter).toList();
}
Now you can easily ask if (filter is Filter).
Whenever you create a field that’s set to a function value, use a typedef to
specify that function’s signature. The following example is from Dart’s Web UI
library:
/** Function to set up the contents of a conditional template. */
typedef void ConditionalBodySetup(ConditionalTemplate template);
/**
* A template conditional like `<template instantiate="if test">` or
* `<td template instantiate="if test">`.
*/
class ConditionalTemplate extends PlaceholderTemplate {
bool isVisible = false;
final ConditionalBodySetup bodySetup;
ConditionalTemplate(Node reference, exp, this.bodySetup)
: super(reference, exp);
...
Type annotations
Why do this:
// JavaScript w/ Closure compiler
/**
* @param {String} name
* @return {String}
*/
makeGreeting = function(name) {
/** @type {String} */
var greeting = 'hello ' + name;
return greeting;
}
When you can do this:
// Dart
String MakeGreeting(String name) {
String greeting = 'hello $name';
return greeting;
}
Dart is an optionally typed language, which means developers don’t need
to fight ceremonial type checkers just to get their code to run. Use type
annotations as “inline documentation” to help your fellow developers and
tools.
Generally speaking, use type annotations on the “surface area” of your code.
If another developer is going to see the interface, use type annotations.
Your friends will thank you.
Inside of methods, the rules are a bit more flexible. Use type annotations
when you want to, but feel free to use var if your team’s style guide
permits it. A good editor or analyzer should perform useful type inference
even if var is used.
It’s perfectly fine to omit a type annotation if you need to express something
the type system can’t naturally express. For example, if your method takes
an integer or a Duration, you can simply use var.
/// This method used to only take an [int] but now it takes a [Duration] or
/// [int]. Use of [int] is deprecated, please use [Duration].
calculateTimePeriod(var duration) {
if (duration is int) {
// ...
} else if (duration is Duration) {
// ...
} else {
throw new ArgumentError('duration must be an int or Duration');
}
}
This is a common practice used while an API is evolving. Once the evolution is
complete, you can add a specific type annotation.
Learn more about Dart’s optional static typing.
Fields, getters, and setters
Speaking of properties, Dart uses your standard object.someProperty syntax for
working with them. That’s how most languages work when someProperty is an
actual field on the class, but Dart also allows you to define methods that look
like property access but execute whatever code you want. As in other languages,
these are called getters and setters. Here’s an example:
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
Here we have a Rectangle class with four actual fields, left, top, width,
and height. It also has getters and setters to define two more logical
properties: right and bottom. If you’re using the class, there is no visible
difference between a “real” field and getters and setters:
var rect = new Rectangle(3, 4, 20, 15);
print(rect.left);
print(rect.bottom);
rect.top = 6;
rect.right = 12;
This blurring the line between fields and getters/setters is fundamental to the
language. The clearest way to think of it is that fields are just getters and
setters with default implementations. This means that you can do fun stuff like
override an inherited getter with a field and vice versa. If an interface
defines a getter, you can implement it by simply having a field with the same
name and type. If the field is mutable (not final) it can implement a setter
that an interface requires.
In practice, what this means is that you don’t have to insulate your fields by
defensively hiding them behind boilerplate getters and setters like you would in
Java or C#. If you have some exposed property, feel free to make it a public
field. If you don’t want it to be modified, just make it final.
Later, if you need to do some validation or other work, you can always replace
that field with a getter and setter. If we wanted our Rectangle class to make
sure it always has a non-negative size, we could change it to:
class Rectangle {
num left, top;
num _width, _height;
Rectangle(this.left, this.top, this._width, this._height);
num get width => _width;
set width(num value) {
if (value < 0) throw 'Width cannot be negative.';
_width = value;
}
num get height => _height;
set height(num value) {
if (value < 0) throw 'Height cannot be negative.';
_height = value;
}
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
And now we’ve modified the class to do some validation without having to touch
any of the existing code that was already using it.
Top-level definitions
Dart is a “pure” object-oriented language in that everything you can place in a
variable is a real object (no mutant “primitives”) and every object is an
instance of some class. It’s not a dogmatic OOP language though. You aren’t
required to place everything you define inside some class. Instead, you are free
to define functions, variables, and even getters and setters at the top level if
you want.
import 'dart:math';
num abs(num value) => value < 0 ? -value : value;
final TWO_PI = PI * 2.0;
int get today {
final date = new DateTime.now();
return date.day;
}
main() {
print(today);
}
Even in languages that don’t require you to place everything inside a class or
object, like JavaScript, it’s still common to do so as a form of namespacing:
top-level definitions with the same name could inadvertently collide. To address
that, Dart has a library system that allows you to import definitions from other
libraries with a prefix applied to disambiguate it. That means you shouldn’t
need to defensively squirrel your definitions inside classes.
The most common example of a top-level function is main(). If you work with
the DOM, the familiar document and window “variables” are actually top-level
getters in Dart. The project used to have a Math class, but we moved all
functionality from that class to top-level methods inside the dart:math library.
Dependency injection
You can combine ideas from typedefs, functions, and constructors to build a
simple dependency injection system. Consider this example:
typedef Connection ConnectionFactory();
Connection _newDBConnection() => new DatabaseConnection();
class Person {
String id;
String name;
ConnectionFactory connectionFactory;
Person({this.connectionFactory: _newDBConnection});
Future save() {
var conn = connectionFactory();
return conn.query('UPDATE PERSONS SET name = ? WHERE id = ?', [name, id]);
}
}
The above sample shows off a bunch of cool features from Dart:
- Typedefs
- Used to create an alias for a function that returns a new database
connection.
- Optional named parameters
- Used to set a default database connection factory,
or use a user-supplied factory function.
- Top-level functions
- Used to define the default database connection factory method.
Strings and interpolation
Dart has a few kinds of string literals. You can use single or double quotes,
and you can use triple-quoted multiline strings:
var s1 = 'I am a "string"'
"I'm one too";
var s2 = '''I'm
on multiple lines
''';
var s3 = """
As
am
I
""";
While there is a plus (+) operator on String, it’s often cleaner and faster to
use string interpolation:
var name = 'Fred';
var salutation = 'Hi';
var greeting = '$salutation, $name';
A dollar sign ($) in a string literal followed by a variable will expand to
that variable’s value. (If the variable isn’t a string, it calls toString() on
it.) You can also interpolate expressions by placing them inside curly braces:
import 'dart:math';
main() {
var r = 2;
print('The area of a circle with radius $r is ${PI * r * r}');
}
Operators
Dart shares the same operators and precedences that you’re familiar with from C,
Java, etc. They will do what you expect. Under the hood, though, they are a
little special. In Dart, an expression using an operator like 1 + 2 is really
just syntactic sugar for calling a method. The previous example looks more like
1.+(2) to the language.
This means that you can also override (most) operators for your own types. For
example, here’s a Vector class:
class Vector {
num x, y;
Vector(this.x, this.y);
operator +(Vector other) => new Vector(x + other.x, y + other.y);
}
With that, we can add vectors using familiar syntax:
var position = new Vector(3, 4);
var velocity = new Vector(1, 2);
var newPosition = position + velocity;
That being said, please don’t go crazy with this. We’re giving you the keys to
the car and trusting that you won’t turn around and drive it through the living
room.
In practice, if the type you’re defining often uses operators in the “real
world” (on a blackboard?) then it might be a good candidate for overridden
operators: things like complex numbers, vectors, matrices, etc. Otherwise,
probably not. Types with custom operators should generally be immutable too.
Note that because operator calls are really just method calls, they have an
inherent asymmetry. The method is always looked up on the left-hand argument. So
when you do a + b, it’s the type of a that gets to decide what that means.
Equality
Dart has two equality operators,
== and !=,
which work a little differently from the JavaScript equality operators.
Unlike JavaScript, Dart has no === operator.
Instead, it has a top-level function called identical().
Use == and != for testing equivalence. They are what you’ll need 99% of the
time. Unlike in JavaScript, they don’t do any implicit conversions, so they
will behave like you’d expect. Don’t be afraid to use them. Unlike Java,
they work for any type that has an equivalence relation defined. No more
someString.equals("something").
You can implement == for your own types if that makes sense for them. You don’t
have to implement !=: Dart will automatically infer that from your definition
of ==. If you do implement == be sure to also implement
hashCode.
The identical() function is for testing whether two objects are the exact
same object in memory. In practice, you will rarely need to use it. The Object
class definition of == returns identical(this, other), so the only time
you’ll need to call identical() is if you overload == or specifically want
to sidestep an overloaded == operator.
Numbers
Dart has a num class and two subclasses: int and double. Integers are of
arbitrary size in the VM, and double are 64-bit doubles as defined by IEEE 754
standard.
In typical Dart code, we find that we want two kinds of numbers:
- Only integers with no floating points. For instance, using ints for list
indices.
- Any number, including floating points.
Using int handles the first set, and using num handles the second set. It’s
very rare that we want a number that must have a floating point and
cannot be an integer, which is what double expresses.
Idiomatic Dart numbers are annotated with either int or num,
rarely double.
Futures
A Future is a promise for a value to be returned, well, in the future.
Methods that work with a Future should always return the Future. This helps
consumers of the method to properly handle errors that might occur. It also
lets consumers know when the operation is complete.
Future doLengthyComputation() {
return lengthyComp().then((value) => print(value))
.catchError((e) => print(e));
}
Always chain the catchError() call off of the call to
then(), otherwise you will lose exceptions thrown from within then().
Here is an example of what not to do:
// WARNING: This code contains an anti-pattern.
Future doLengthyComputation() {
Future future = lengthyComp();
future.then((value) => print(value));
// BAD! You'll only get errors from future, not from then().
// BAD! Your caller never sees any errors that occur.
future.catchError((e) => print(e));
return future;
}
If you want to run a function “in the future”, it’s tempting to use
Timer.run. Unless you know what you’re doing, don’t.
Unfortunately, exceptions thrown
from within run’s callback are more-or-less uncatchable.
Luckily, Future has a constructor that can help. Use
Future.delayed to run a function in a future event loop tick
without losing exceptions that might be thrown.
Future doLengthyComputation() {
return new Future.delayed(const Duration(seconds: 0),
() => doTheThingThatMightFail());
}
Dart supports structured comments that can be parsed by tools. However, Dart
eschews ceremonial API docs for more fluid and natural comments.
Compare and contrast Java and Dart comment styles:
/**
* Returns an Llama object that can then be petted.
* The age argument must specify an non-zero integer. The amount
* argument is the amount of {@link Money} paid for the llama.
* <p>
* This method throws {@link NoMoreLlamasException} is thrown
* if there are no more llamas to purchase.
* {@link IllegalArgumentException} is
* thrown if age is less than zero.
*
* @param age a non-zero age
* @param amount the amount of money paid for the llama
* @throws NoMoreLlamasException if there are no more llamas available
* @throws IllegalArgumentException if age is less than zero
* @return the llama
* @see Farmer
*/
public Llama buyLlama(Age age, Money amount) {
// ...
}
/**
* Returns a Llama that can be petted.
* An [ArgumentError] is thrown if age is less than zero, and
* [NoMoreLlamasError] is thrown if they are all out of llamas.
*/
Llama buyLlama(int age, Money amount) {
// ...
}
Less is more with Dart doc comments. No need to repeat yourself over and over,
just say what you need to say inline in the comments.
Also, no need to embed HTML tags in your doc comments; Dart’s dartdoc tool can
understand a subset of markdown.
/**
* ## Examples
*
* Getting the _value_:
*
* Future<int> future = getFutureFromSomewhere();
* future.then((value) {
* print("I received the number $value");
* });
* ...
*/
Learn more about Dart’s comment styles.