Effective Dart: Usage

You can use these guidelines every day in the bodies of your Dart code. Users_of your library may not be able to tell that you’ve internalized the ideas here,but _maintainers of it sure will.

Libraries

These guidelines help you compose your program out of multiple files in aconsistent, maintainable way. To keep these guidelines brief, they use “import”to cover import and export directives. The guidelines apply equally to both.

DO use strings in part of directives.

Many Dart developers avoid using part entirely. They find it easier to reasonabout their code when each library is a single file. If you do choose to usepart to split part of a library out into another file, Dart requires the otherfile to in turn indicate which library it’s a part of. For legacy reasons, Dartallows this part of directive to use the name of the library it’s a part of.That makes it harder for tools to physically find the main library file, and canmake it ambiguous which library the part is actually part of.

The preferred, modern syntax is to use a URI string that points directly to thelibrary file, just like you use in other directives. If you have some library,my_library.dart, that contains:

  1. library my_library;
  2.  
  3. part "some/other/file.dart";

Then the part file should look like:

  1. part of "../../my_library.dart";

And not:

  1. part of my_library;

DON’T import libraries that are inside the src directory of another package.

The src directory under lib is specified to containlibraries private to the package’s own implementation. The way packagemaintainers version their package takes this convention into account. They arefree to make sweeping changes to code under src without it being a breakingchange to the package.

That means that if you import some other package’s private library, a minor,theoretically non-breaking point release of that package could break your code.

PREFER relative paths when importing libraries within your own package’s lib directory.

Linter rule: avoid_relative_lib_imports

When referencing a library inside your package’s lib directory from anotherlibrary in that same package, either a relative URI or an explicit package:will work.

For example, say your directory structure looks like:

  1. my_package
  2. └─ lib
  3. ├─ src
  4. └─ utils.dart
  5. └─ api.dart

If api.dart wants to import utils.dart, it should do so using:

  1. import 'src/utils.dart';

And not:

  1. import 'package:my_package/src/utils.dart';

There is no profound reason to prefer the former—it’s just shorter, and we wantto be consistent.

The “within your own package’s lib directory” part is important. Librariesinside lib can import other libraries inside lib (or in subdirectories ofit). Libraries outside of lib can use relative imports to reach otherlibraries outside of lib. For example, you may have a test utility libraryunder test that other libraries in test import.

But you can’t “cross the streams”. A library outside of lib should never use arelative import to reach a library under lib, or vice versa. Doing so willbreak Dart’s ability to correctly tell if two library URIs refer to the samelibrary. Follow these two rules:

  • An import path should never contain /lib/.
  • A library under lib should never use ../ to escape the lib directory.

Booleans

DO use ?? to convert null to a boolean value.

This rule applies when an expression can evaluate true, false, or null,and you need to pass the result to something that doesn’t accept null. Acommon case is the result of a null-aware method call being used as a condition:

  1. if (optionalThing?.isEnabled) {
  2. print("Have enabled thing.");
  3. }

This code throws an exception if optionalThing is null. To fix this, youneed to “convert” the null value to either true or false. Although youcould do this using ==, we recommend using ??:

  1. // If you want null to be false:
  2. optionalThing?.isEnabled ?? false;
  3.  
  4. // If you want null to be true:
  5. optionalThing?.isEnabled ?? true;
  1. // If you want null to be false:
  2. optionalThing?.isEnabled == true;
  3.  
  4. // If you want null to be true:
  5. optionalThing?.isEnabled != false;

Both operations produce the same result and do the right thing, but ?? ispreferred for three main reasons:

  • The ?? operator clearly signals that the code has something to do with anull value.

  • The == true looks like a common new programmer mistake where the equalityoperator is redundant and can be removed. That’s true when the booleanexpression on the left will not produce null, but not when it can.

  • The ?? false and ?? true clearly show what value will be used when theexpression is null. With == true, you have to think through the booleanlogic to realize that means that a null gets converted to false.

Strings

Here are some best practices to keep in mind when composing strings in Dart.

DO use adjacent strings to concatenate string literals.

Linter rule: prefer_adjacent_string_concatenation

If you have two string literals—not values, but the actual quoted literalform—you do not need to use + to concatenate them. Just like in C andC++, simply placing them next to each other does it. This is a good way to makea single long string that doesn’t fit on one line.

  1. raiseAlarm(
  2. 'ERROR: Parts of the spaceship are on fire. Other '
  3. 'parts are overrun by martians. Unclear which are which.');
  1. raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
  2. 'parts are overrun by martians. Unclear which are which.');

PREFER using interpolation to compose strings and values.

Linter rule: prefer_interpolation_to_compose_strings

If you’re coming from other languages, you’re used to using long chains of +to build a string out of literals and other values. That does work in Dart, butit’s almost always cleaner and shorter to use interpolation:

  1. 'Hello, $name! You are ${year - birth} years old.';
  1. 'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

AVOID using curly braces in interpolation when not needed.

Linter rule: unnecessary_brace_in_string_interps

If you’re interpolating a simple identifier not immediately followed by morealphanumeric text, the {} should be omitted.

  1. 'Hi, $name!'
  2. "Wear your wildest $decade's outfit."
  3. 'Wear your wildest ${decade}s outfit.'
  1. 'Hi, ${name}!'
  2. "Wear your wildest ${decade}'s outfit."

Collections

Out of the box, Dart supports four collection types: lists, maps, queues, and sets.The following best practices apply to collections.

DO use collection literals when possible.

Linter rule: prefer_collection_literals

There are two ways to make an empty growable list: [] and List().Likewise, there are three ways to make an empty linked hash map: {},Map(), and LinkedHashMap().

If you want to create a non-growable list, or some other custom collection typethen, by all means, use a constructor. Otherwise, use the nice literal syntax.The core library exposes those constructors to ease adoption, but idiomatic Dartcode does not use them.

  1. var points = [];
  2. var addresses = {};
  1. var points = List();
  2. var addresses = Map();

You can even provide a type argument for them if that matters.

  1. var points = <Point>[];
  2. var addresses = <String, Address>{};
  1. var points = List<Point>();
  2. var addresses = Map<String, Address>();

Note that this doesn’t apply to the named constructors for those classes.List.from(), Map.fromIterable(), and friends all have their uses. Likewise,if you’re passing a size to List() to create a non-growable one, then itmakes sense to use that.

DON’T use .length to see if a collection is empty.

The Iterable contract does not require that a collection know its length orbe able to provide it in constant time. Calling .length just to see if thecollection contains anything can be painfully slow.

Instead, there are faster and more readable getters: .isEmpty and.isNotEmpty. Use the one that doesn’t require you to negate the result.

  1. if (lunchBox.isEmpty) return 'so hungry...';
  2. if (words.isNotEmpty) return words.join(' ');
  1. if (lunchBox.length == 0) return 'so hungry...';
  2. if (!words.isEmpty) return words.join(' ');

CONSIDER using higher-order methods to transform a sequence.

If you have a collection and want to produce a new modified collection from it,it’s often shorter and more declarative to use .map(), .where(), and theother handy methods on Iterable.

Using those instead of an imperative for loop makes it clear that your intentis to produce a new sequence and not to produce side effects.

  1. var aquaticNames = animals
  2. .where((animal) => animal.isAquatic)
  3. .map((animal) => animal.name);

At the same time, this can be taken too far. If you are chaining or nestingmany higher-order methods, it may be clearer to write a chunk of imperativecode.

AVOID using Iterable.forEach() with a function literal.

Linter rule: avoid_function_literals_in_foreach_calls

forEach() functions are widely used in JavaScript because the built infor-in loop doesn’t do what you usually want. In Dart, if you want to iterateover a sequence, the idiomatic way to do that is using a loop.

  1. for (var person in people) {
  2. ...
  3. }
  1. people.forEach((person) {
  2. ...
  3. });

Note that this guideline specifically says “function literal”. If you want toinvoke some already existing function on each element, forEach() is fine.

  1. people.forEach(print);

Also note that it’s always OK to use Map.forEach(). Maps aren’t iterable, sothis guideline doesn’t apply.

DON’T use List.from() unless you intend to change the type of the result.

Given an Iterable, there are two obvious ways to produce a new List thatcontains the same elements:

  1. var copy1 = iterable.toList();
  2. var copy2 = List.from(iterable);

The obvious difference is that the first one is shorter. The _important_difference is that the first one preserves the type argument of the originalobject:

  1. // Creates a List<int>:
  2. var iterable = [1, 2, 3];
  3.  
  4. // Prints "List<int>":
  5. print(iterable.toList().runtimeType);
  1. // Creates a List<int>:
  2. var iterable = [1, 2, 3];
  3.  
  4. // Prints "List<dynamic>":
  5. print(List.from(iterable).runtimeType);

If you want to change the type, then calling List.from() is useful:

  1. var numbers = [1, 2.3, 4]; // List<num>.
  2. numbers.removeAt(1); // Now it only contains integers.
  3. var ints = List<int>.from(numbers);

But if your goal is just to copy the iterable and preserve its original type, oryou don’t care about the type, then use toList().

DO use whereType() to filter a collection by type.

Linter rule: prefer_iterable_whereType

Let’s say you have a list containing a mixture of objects, and you want to getjust the integers out of it. You could use where() like this:

  1. var objects = [1, "a", 2, "b", 3];
  2. var ints = objects.where((e) => e is int);

This is verbose, but, worse, it returns an iterable whose type probably isn’twhat you want. In the example here, it returns an Iterable<Object> even thoughyou likely want an Iterable<int> since that’s the type you’re filtering it to.

Sometimes you see code that “corrects” the above error by adding cast():

  1. var objects = [1, "a", 2, "b", 3];
  2. var ints = objects.where((e) => e is int).cast<int>();

That’s verbose and causes two wrappers to be created, with two layers ofindirection and redundant runtime checking. Fortunately, the core library hasthe whereType() method for this exact use case:

  1. var objects = [1, "a", 2, "b", 3];
  2. var ints = objects.whereType<int>();

Using whereType() is concise, produces an Iterable of the desired type,and has no unnecessary levels of wrapping.

DON’T use cast() when a nearby operation will do.

Often when you’re dealing with an iterable or stream, you perform severaltransformations on it. At the end, you want to produce an object with a certaintype argument. Instead of tacking on a call to cast(), see if one of theexisting transformations can change the type.

If you’re already calling toList(), replace that with a call toList<T>.from() where T is the type of resulting list you want.

  1. var stuff = <dynamic>[1, 2];
  2. var ints = List<int>.from(stuff);
  1. var stuff = <dynamic>[1, 2];
  2. var ints = stuff.toList().cast<int>();

If you are calling map(), give it an explicit type argument so that itproduces an iterable of the desired type. Type inference often picks the correcttype for you based on the function you pass to map(), but sometimes you needto be explicit.

  1. var stuff = <dynamic>[1, 2];
  2. var reciprocals = stuff.map<double>((n) => 1 / n);
  1. var stuff = <dynamic>[1, 2];
  2. var reciprocals = stuff.map((n) => 1 / n).cast<double>();

AVOID using cast().

This is the softer generalization of the previous rule. Sometimes there is nonearby operation you can use to fix the type of some object. Even then, whenpossible avoid using cast() to “change” a collection’s type.

Prefer any of these options instead:

  • Create it with the right type. Change the code where the collection isfirst created so that it has the right type.

  • Cast the elements on access. If you immediately iterate over thecollection, cast each element inside the iteration.

  • Eagerly cast using List.from(). If you’ll eventually access most ofthe elements in the collection, and you don’t need the object to be backedby the original live object, convert it using List.from().

The cast() method returns a lazy collection that checks the element typeon every operation. If you perform only a few operations on only a fewelements, that laziness can be good. But in many cases, the overhead of lazyvalidation and of wrapping outweighs the benefits.

Here is an example of creating it with the right type:

  1. List<int> singletonList(int value) {
  2. var list = <int>[];
  3. list.add(value);
  4. return list;
  5. }
  1. List<int> singletonList(int value) {
  2. var list = []; // List<dynamic>.
  3. list.add(value);
  4. return list.cast<int>();
  5. }

Here is casting each element on access:

  1. void printEvens(List<Object> objects) {
  2. // We happen to know the list only contains ints.
  3. for (var n in objects) {
  4. if ((n as int).isEven) print(n);
  5. }
  6. }
  1. void printEvens(List<Object> objects) {
  2. // We happen to know the list only contains ints.
  3. for (var n in objects.cast<int>()) {
  4. if (n.isEven) print(n);
  5. }
  6. }

Here is casting eagerly using List.from():

  1. int median(List<Object> objects) {
  2. // We happen to know the list only contains ints.
  3. var ints = List<int>.from(objects);
  4. ints.sort();
  5. return ints[ints.length ~/ 2];
  6. }
  1. int median(List<Object> objects) {
  2. // We happen to know the list only contains ints.
  3. var ints = objects.cast<int>();
  4. ints.sort();
  5. return ints[ints.length ~/ 2];
  6. }

These alternatives don’t always work, of course, and sometimes cast() is theright answer. But consider that method a little risky and undesirable—itcan be slow and may fail at runtime if you aren’t careful.

Functions

In Dart, even functions are objects. Here are some best practicesinvolving functions.

DO use a function declaration to bind a function to a name.

Linter rule: prefer_function_declarations_over_variables

Modern languages have realized how useful local nested functions and closuresare. It’s common to have a function defined inside another one. In many cases,this function is used as a callback immediately and doesn’t need a name. Afunction expression is great for that.

But, if you do need to give it a name, use a function declaration statementinstead of binding a lambda to a variable.

  1. void main() {
  2. localFunction() {
  3. ...
  4. }
  5. }
  1. void main() {
  2. var localFunction = () {
  3. ...
  4. };
  5. }

DON’T create a lambda when a tear-off will do.

Linter rule: unnecessary_lambdas

If you refer to a method on an object but omit the parentheses, Dart gives youa “tear-off”—a closure that takes the same parameters as the method andinvokes it when you call it.

If you have a function that invokes a method with the same arguments as arepassed to it, you don’t need to manually wrap the call in a lambda.

  1. names.forEach(print);
  1. names.forEach((name) {
  2. print(name);
  3. });

Parameters

DO use = to separate a named parameter from its default value.

Linter rule: prefer_equal_for_default_values

For legacy reasons, Dart allows both : and = as the default value separatorfor named parameters. For consistency with optional positional parameters, use=.

  1. void insert(Object item, {int at = 0}) { ... }
  1. void insert(Object item, {int at: 0}) { ... }

DON’T use an explicit default value of null.

Linter rule: avoid_init_to_null

If you make a parameter optional but don’t give it a default value, the languageimplicitly uses null as the default, so there’s no need to write it.

  1. void error([String message]) {
  2. stderr.write(message ?? '\n');
  3. }
  1. void error([String message = null]) {
  2. stderr.write(message ?? '\n');
  3. }

Variables

The following best practices describe how to best use variables in Dart.

DON’T explicitly initialize variables to null.

In Dart, a variable or field that is not explicitly initialized automaticallygets initialized to null. This is reliably specified by the language. There’sno concept of “uninitialized memory” in Dart. Adding = null is redundant andunneeded.

  1. int _nextId;
  2.  
  3. class LazyId {
  4. int _id;
  5.  
  6. int get id {
  7. if (_nextId == null) _nextId = 0;
  8. if (_id == null) _id = _nextId++;
  9.  
  10. return _id;
  11. }
  12. }
  1. int _nextId = null;
  2.  
  3. class LazyId {
  4. int _id = null;
  5.  
  6. int get id {
  7. if (_nextId == null) _nextId = 0;
  8. if (_id == null) _id = _nextId++;
  9.  
  10. return _id;
  11. }
  12. }

AVOID storing what you can calculate.

When designing a class, you often want to expose multiple views into the sameunderlying state. Often you see code that calculates all of those views in theconstructor and then stores them:

  1. class Circle {
  2. num radius;
  3. num area;
  4. num circumference;
  5.  
  6. Circle(num radius)
  7. : radius = radius,
  8. area = pi * radius * radius,
  9. circumference = pi * 2.0 * radius;
  10. }

This code has two things wrong with it. First, it’s likely wasting memory. Thearea and circumference, strictly speaking, are caches. They are storedcalculations that we could recalculate from other data we already have. They aretrading increased memory for reduced CPU usage. Do we know we have a performanceproblem that merits that trade-off?

Worse, the code is wrong. The problem with caches is invalidation—howdo you know when the cache is out of date and needs to be recalculated? Here, wenever do, even though radius is mutable. You can assign a different value andthe area and circumference will retain their previous, now incorrect values.

To correctly handle cache invalidation, we need to do this:

  1. class Circle {
  2. num _radius;
  3. num get radius => _radius;
  4. set radius(num value) {
  5. _radius = value;
  6. _recalculate();
  7. }
  8.  
  9. num _area;
  10. num get area => _area;
  11.  
  12. num _circumference;
  13. num get circumference => _circumference;
  14.  
  15. Circle(this._radius) {
  16. _recalculate();
  17. }
  18.  
  19. void _recalculate() {
  20. _area = pi * _radius * _radius;
  21. _circumference = pi * 2.0 * _radius;
  22. }
  23. }

That’s an awful lot of code to write, maintain, debug, and read. Instead, yourfirst implementation should be:

  1. class Circle {
  2. num radius;
  3.  
  4. Circle(this.radius);
  5.  
  6. num get area => pi * radius * radius;
  7. num get circumference => pi * 2.0 * radius;
  8. }

This code is shorter, uses less memory, and is less error-prone. It stores theminimal amount of data needed to represent the circle. There are no fields toget out of sync because there is only a single source of truth.

In some cases, you may need to cache the result of a slow calculation, but onlydo that after you know you have a performance problem, do it carefully, andleave a comment explaining the optimization.

Members

In Dart, objects have members which can be functions (methods) or data (instancevariables). The following best practices apply to an object’s members.

DON’T wrap a field in a getter and setter unnecessarily.

Linter rule: unnecessary_getters_setters

In Java and C#, it’s common to hide all fields behind getters and setters (orproperties in C#), even if the implementation just forwards to the field. Thatway, if you ever need to do more work in those members, you can without needingto touch the callsites. This is because calling a getter method is differentthan accessing a field in Java, and accessing a property isn’t binary-compatiblewith accessing a raw field in C#.

Dart doesn’t have this limitation. Fields and getters/setters are completelyindistinguishable. You can expose a field in a class and later wrap it in agetter and setter without having to touch any code that uses that field.

  1. class Box {
  2. var contents;
  3. }
  1. class Box {
  2. var _contents;
  3. get contents => _contents;
  4. set contents(value) {
  5. _contents = value;
  6. }
  7. }

PREFER using a final field to make a read-only property.

Linter rule: unnecessary_getters

If you have a field that outside code should be able to see but not assign to, asimple solution that works in many cases is to simply mark it final.

  1. class Box {
  2. final contents = [];
  3. }
  1. class Box {
  2. var _contents;
  3. get contents => _contents;
  4. }

Of course, if you need to internally assign to the field outside of theconstructor, you may need to do the “private field, public getter” pattern, butdon’t reach for that until you need to.

CONSIDER using => for simple members.

Linter rule: prefer_expression_function_bodies

In addition to using => for function expressions, Dart also lets you definemembers with it. That style is a good fit for simple members that just calculateand return a value.

  1. double get area => (right - left) * (bottom - top);
  2.  
  3. bool isReady(num time) => minTime == null || minTime <= time;
  4.  
  5. String capitalize(String name) =>
  6. '${name[0].toUpperCase()}${name.substring(1)}';

People writing code seem to love =>, but it’s very easy to abuse it and endup with code that’s hard to read. If your declaration is more than a couple oflines or contains deeply nested expressions—cascades and conditionaloperators are common offenders—do yourself and everyone who has to readyour code a favor and use a block body and some statements.

  1. Treasure openChest(Chest chest, Point where) {
  2. if (_opened.containsKey(chest)) return null;
  3.  
  4. var treasure = Treasure(where);
  5. treasure.addAll(chest.contents);
  6. _opened[chest] = treasure;
  7. return treasure;
  8. }
  1. Treasure openChest(Chest chest, Point where) =>
  2. _opened.containsKey(chest) ? null : _opened[chest] = Treasure(where)
  3. ..addAll(chest.contents);

You can also use => on members that don’t return a value. This is idiomaticwhen a setter is small and has a corresponding getter that uses =>.

  1. num get x => center.x;
  2. set x(num value) => center = Point(value, center.y);

DON’T use this. except to redirect to a named constructor or to avoid shadowing.

Linter rule: unnecessary_this

JavaScript requires an explicit this. to refer to members on the object whosemethod is currently being executed, but Dart—like C++, Java, andC#—doesn’t have that limitation.

There are only two times you need to use this.. One is when a local variablewith the same name shadows the member you want to access:

  1. class Box {
  2. var value;
  3.  
  4. void clear() {
  5. this.update(null);
  6. }
  7.  
  8. void update(value) {
  9. this.value = value;
  10. }
  11. }
  1. class Box {
  2. var value;
  3.  
  4. void clear() {
  5. update(null);
  6. }
  7.  
  8. void update(value) {
  9. this.value = value;
  10. }
  11. }

The other time to use this. is when redirecting to a named constructor:

  1. class ShadeOfGray {
  2. final int brightness;
  3.  
  4. ShadeOfGray(int val) : brightness = val;
  5.  
  6. ShadeOfGray.black() : this(0);
  7.  
  8. // This won't parse or compile!
  9. // ShadeOfGray.alsoBlack() : black();
  10. }
  1. class ShadeOfGray {
  2. final int brightness;
  3.  
  4. ShadeOfGray(int val) : brightness = val;
  5.  
  6. ShadeOfGray.black() : this(0);
  7.  
  8. // But now it will!
  9. ShadeOfGray.alsoBlack() : this.black();
  10. }

Note that constructor parameters never shadow fields in constructorinitialization lists:

  1. class Box extends BaseBox {
  2. var value;
  3.  
  4. Box(value)
  5. : value = value,
  6. super(value);
  7. }

This looks surprising, but works like you want. Fortunately, code like this isrelatively rare thanks to initializing formals.

DO initialize fields at their declaration when possible.

If a field doesn’t depend on any constructor parameters, it can and should beinitialized at its declaration. It takes less code and makes sure you won’tforget to initialize it if the class has multiple constructors.

  1. class Folder {
  2. final String name;
  3. final List<Document> contents;
  4.  
  5. Folder(this.name) : contents = [];
  6. Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
  7. }
  1. class Folder {
  2. final String name;
  3. final List<Document> contents = [];
  4.  
  5. Folder(this.name);
  6. Folder.temp() : name = 'temporary';
  7. }

Of course, if a field depends on constructor parameters, or is initializeddifferently by different constructors, then this guideline does not apply.

Constructors

The following best practices apply to declaring constructors for a class.

DO use initializing formals when possible.

Linter rule: prefer_initializing_formals

Many fields are initialized directly from a constructor parameter, like:

  1. class Point {
  2. num x, y;
  3. Point(num x, num y) {
  4. this.x = x;
  5. this.y = y;
  6. }
  7. }

We’ve got to type x four times here to define a field. We can do better:

  1. class Point {
  2. num x, y;
  3. Point(this.x, this.y);
  4. }

This this. syntax before a constructor parameter is called an “initializingformal”. You can’t always take advantage of it. Sometimes you want to have anamed parameter whose name doesn’t match the name of the field you areinitializing. But when you can use initializing formals, you should.

DON’T type annotate initializing formals.

Linter rule: type_init_formals

If a constructor parameter is using this. to initialize a field, then the typeof the parameter is understood to be the same type as the field.

  1. class Point {
  2. int x, y;
  3. Point(this.x, this.y);
  4. }
  1. class Point {
  2. int x, y;
  3. Point(int this.x, int this.y);
  4. }

DO use ; instead of {} for empty constructor bodies.

Linter rule: empty_constructor_bodies

In Dart, a constructor with an empty body can be terminated with just asemicolon. (In fact, it’s required for const constructors.)

  1. class Point {
  2. int x, y;
  3. Point(this.x, this.y);
  4. }
  1. class Point {
  2. int x, y;
  3. Point(this.x, this.y) {}
  4. }

DON’T use new.

Linter rule: unnecessary_new

Dart 2 makes the new keyword optional. Even in Dart 1, its meaning was neverclear because factory constructors mean a new invocation may still notactually return a new object.

The language still permits new in order to make migration less painful, butconsider it deprecated and remove it from your code.

  1. Widget build(BuildContext context) {
  2. return Row(
  3. children: [
  4. RaisedButton(
  5. child: Text('Increment'),
  6. ),
  7. Text('Click!'),
  8. ],
  9. );
  10. }
  1. Widget build(BuildContext context) {
  2. return new Row(
  3. children: [
  4. new RaisedButton(
  5. child: new Text('Increment'),
  6. ),
  7. new Text('Click!'),
  8. ],
  9. );
  10. }

DON’T use const redundantly.

Linter rule: unnecessary_const

In contexts where an expression must be constant, the const keyword isimplicit, doesn’t need to be written, and shouldn’t. Those contexts are anyexpression inside:

  • A const collection literal.
  • A const constructor call
  • A metadata annotation.
  • The initializer for a const variable declaration.
  • A switch case expression—the part right after case before the :, notthe body of the case.

(Default values are not included in this list because future versions of Dartmay support non-const default values.)

Basically, any place where it would be an error to write new instead ofconst, Dart 2 allows you to omit the const.

  1. const primaryColors = [
  2. Color("red", [255, 0, 0]),
  3. Color("green", [0, 255, 0]),
  4. Color("blue", [0, 0, 255]),
  5. ];
  1. const primaryColors = const [
  2. const Color("red", const [255, 0, 0]),
  3. const Color("green", const [0, 255, 0]),
  4. const Color("blue", const [0, 0, 255]),
  5. ];

Error handling

Dart uses exceptions when an error occurs in your program. The followingbest practices apply to catching and throwing exceptions.

AVOID catches without on clauses.

Linter rule: avoid_catches_without_on_clauses

A catch clause with no on qualifier catches anything thrown by the code inthe try block. Pokémon exception handling is very likely not what youwant. Does your code correctly handle StackOverflowError orOutOfMemoryError? If you incorrectly pass the wrong argument to a method inthat try block do you want to have your debugger point you to the mistake orwould you rather that helpful ArgumentError get swallowed? Do you want anyassert() statements inside that code to effectively vanish since you’recatching the thrown AssertionErrors?

The answer is probably “no”, in which case you should filter the types youcatch. In most cases, you should have an on clause that limits you to thekinds of runtime failures you are aware of and are correctly handling.

In rare cases, you may wish to catch any runtime error. This is usually inframework or low-level code that tries to insulate arbitrary application codefrom causing problems. Even here, it is usually better to catch Exceptionthan to catch all types. Exception is the base class for all runtime errorsand excludes errors that indicate programmatic bugs in the code.

DON’T discard errors from catches without on clauses.

If you really do feel you need to catch everything that can be thrown from aregion of code, do something with what you catch. Log it, display it to theuser or rethrow it, but do not silently discard it.

DO throw objects that implement Error only for programmatic errors.

The Error class is the base class for programmatic errors. When an objectof that type or one of its subinterfaces like ArgumentError is thrown, itmeans there is a bug in your code. When your API wants to report to a callerthat it is being used incorrectly throwing an Error sends that signal clearly.

Conversely, if the exception is some kind of runtime failure that doesn’tindicate a bug in the code, then throwing an Error is misleading. Instead, throwone of the core Exception classes or some other type.

DON’T explicitly catch Error or types that implement it.

This follows from the above. Since an Error indicates a bug in your code, itshould unwind the entire callstack, halt the program, and print a stack trace soyou can locate and fix the bug.

Catching errors of these types breaks that process and masks the bug. Instead ofadding error-handling code to deal with this exception after the fact, go backand fix the code that is causing it to be thrown in the first place.

DO use rethrow to rethrow a caught exception.

Linter rule: use_rethrow_when_possible

If you decide to rethrow an exception, prefer using the rethrow statementinstead of throwing the same exception object using throw.rethrow preserves the original stack trace of the exception. throw on theother hand resets the stack trace to the last thrown position.

  1. try {
  2. somethingRisky();
  3. } catch (e) {
  4. if (!canHandle(e)) throw e;
  5. handle(e);
  6. }
  1. try {
  2. somethingRisky();
  3. } catch (e) {
  4. if (!canHandle(e)) rethrow;
  5. handle(e);
  6. }

Asynchrony

Dart has several language features to support asynchronous programming.The following best practices apply to asynchronous coding.

PREFER async/await over using raw futures.

Asynchronous code is notoriously hard to read and debug, even when using a niceabstraction like futures. The async/await syntax improves readability andlets you use all of the Dart control flow structures within your async code.

  1. Future<int> countActivePlayers(String teamName) async {
  2. try {
  3. var team = await downloadTeam(teamName);
  4. if (team == null) return 0;
  5.  
  6. var players = await team.roster;
  7. return players.where((player) => player.isActive).length;
  8. } catch (e) {
  9. log.error(e);
  10. return 0;
  11. }
  12. }
  1. Future<int> countActivePlayers(String teamName) {
  2. return downloadTeam(teamName).then((team) {
  3. if (team == null) return Future.value(0);
  4.  
  5. return team.roster.then((players) {
  6. return players.where((player) => player.isActive).length;
  7. });
  8. }).catchError((e) {
  9. log.error(e);
  10. return 0;
  11. });
  12. }

DON’T use async when it has no useful effect.

It’s easy to get in the habit of using async on any function that doesanything related to asynchrony. But in some cases, it’s extraneous. If you canomit the async without changing the behavior of the function, do so.

  1. Future afterTwoThings(Future first, Future second) {
  2. return Future.wait([first, second]);
  3. }
  1. Future afterTwoThings(Future first, Future second) async {
  2. return Future.wait([first, second]);
  3. }

Cases where async is useful include:

  • You are using await. (This is the obvious one.)

  • You are returning an error asynchronously. async and then throw is shorterthan return Future.error(…).

  • You are returning a value and you want it implicitly wrapped in a future.async is shorter than Future.value(…).

  1. Future usesAwait(Future later) async {
  2. print(await later);
  3. }
  4.  
  5. Future asyncError() async {
  6. throw 'Error!';
  7. }
  8.  
  9. Future asyncValue() async => 'value';

CONSIDER using higher-order methods to transform a stream.

This parallels the above suggestion on iterables. Streams support many of thesame methods and also handle things like transmitting errors, closing, etc.correctly.

AVOID using Completer directly.

Many people new to asynchronous programming want to write code that produces afuture. The constructors in Future don’t seem to fit their need so theyeventually find the Completer class and use that.

  1. Future<bool> fileContainsBear(String path) {
  2. var completer = Completer<bool>();
  3.  
  4. File(path).readAsString().then((contents) {
  5. completer.complete(contents.contains('bear'));
  6. });
  7.  
  8. return completer.future;
  9. }

Completer is needed for two kinds of low-level code: new asynchronousprimitives, and interfacing with asynchronous code that doesn’t use futures.Most other code should use async/await or Future.then(), becausethey’re clearer and make error handling easier.

  1. Future<bool> fileContainsBear(String path) {
  2. return File(path).readAsString().then((contents) {
  3. return contents.contains('bear');
  4. });
  5. }
  1. Future<bool> fileContainsBear(String path) async {
  2. var contents = await File(path).readAsString();
  3. return contents.contains('bear');
  4. }

DO test for Future<T> when disambiguating a FutureOr<T> whose type argument could be Object.

Before you can do anything useful with a FutureOr<T>, you typically need to doan is check to see if you have a Future<T> or a bare T. If the typeargument is some specific type as in FutureOr<int>, it doesn’t matter whichtest you use, is int or is Future<int>. Either works because those two typesare disjoint.

However, if the value type is Object or a type parameter that could possiblybe instantiated with Object, then the two branches overlap. Future<Object>itself implements Object, so is Object or is T where T is some typeparameter that could be instantiated with Object returns true even when theobject is a future. Instead, explicitly test for the Future case:

  1. Future<T> logValue<T>(FutureOr<T> value) async {
  2. if (value is Future<T>) {
  3. var result = await value;
  4. print(result);
  5. return result;
  6. } else {
  7. print(value);
  8. return value as T;
  9. }
  10. }
  1. Future<T> logValue<T>(FutureOr<T> value) async {
  2. if (value is T) {
  3. print(value);
  4. return value;
  5. } else {
  6. var result = await value;
  7. print(result);
  8. return result;
  9. }
  10. }

In the bad example, if you pass it a Future<Object>, it incorrectly treats itlike a bare, synchronous value.