Effective Dart: Usage
Libraries
The preferred syntax is to use a URI string that points directly to the library file. If you have some library, my_library.dart
, that contains:
library my_library;
part 'some/other/file.dart';
Then the part file should use the library file's URI string:
part of '../../my_library.dart';
DON'T import libraries that are inside the src
directory of another package
The
src
directory underlib
is specified to contain libraries private to the package's own implementation.They are free to make sweeping changes to code under
src
without it being a breaking change 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.
DON'T allow an import path to reach into or out of lib
A package: import lets you access a library inside a package's lib directory without having to worry about where the package is stored on your computer.
For this to work, you cannot have imports that require the
lib
to be in some location on disk relative to other files.For example, say your directory structure looks like this:
my_package └─ lib └─ api.dart test └─ api_test.dart
And say
api_test.dart
importsapi.dart
in two ways:import 'package:my_package/api.dart';
PREFER relative import paths
When an import does not reach across
lib
, prefer using relative imports.say your directory structure looks like this:
my_package └─ lib ├─ src │ └─ stuff.dart │ └─ utils.dart └─ api.dart test │─ api_test.dart └─ test_utils.dart
Here is how the various libraries should import each other:
lib/api.dart
import 'src/stuff.dart'; import 'src/utils.dart';
lib/src/utils.dart
import '../api.dart'; import 'stuff.dart';
test/api_test.dart
import 'package:my_package/api.dart'; // Don't reach into 'lib'. import 'test_utils.dart'; // Relative within
Null
DON'T explicitly initialize variables to null
If a variable has a non-nullable type, Dart reports a compile error if you try to use it before it has been definitely initialized.
If the variable is nullable, then it is implicitly initialized to
null
for you.Item? bestDeal(List<Item> cart) { Item? bestItem; for (final item in cart) { if (bestItem == null || item.price < bestItem.price) { bestItem = item; } } return bestItem; }
DON'T use an explicit default value of null
DON'T use true
or false
in equality operations
good:
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
bad:
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
- To evaluate a boolean expression that is nullable, you should use
??
or an explicit!= null
check.
AVOID late
variables if you need to check whether they are initialized
Strings:
PREFER using interpolation to compose strings and values
'Hello, $name! You are ${year - birth} years old.';
AVOID using curly braces in interpolation when not needed
var greeting = 'Hi, $name! I love your ${decade}s costume.';
Collections:
DO use collection literals when possible
Dart has three core collection types: List, Map, and Set. The Map and Set classes have unnamed constructors like most classes do.
But because these collections are used so frequently, Dart has nicer built-in syntax for creating them:
good:
var points = <Point>[]; var addresses = <String, Address>{}; var counts = <int>{};
bad:
var addresses = Map<String, Address>(); var counts = Set<int>();
Note that this guideline doesn't apply to the named constructors for those classes.
DON'T use .length to see if a collection is empty
require you to negate the result.
- good:
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
- bad:
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
AVOID using Iterable.forEach()
with a function literal
In Dart, if you want to iterate over a sequence, the idiomatic way to do that is using a loop.
for (final person in people) {
...
}
If you want to invoke some already existing function on each element, forEach()
is fine.
people.forEach(print);
DON'T use List.from()
unless you intend to change the type of the result
Functions:
DO use a function declaration to bind a function to a name
void main() {
void localFunction() {
...
}
}
DON'T create a lambda when a tear-off will do
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach(print);
// Method:
charCodes.forEach(buffer.write);
// Named constructor:
var strings = charCodes.map(String.fromCharCode);
// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
When you refer to a function, method, or named constructor but omit the parentheses, Dart creates a tear-off—a closure that takes the same parameters as the function and invokes the underlying function when you call it.
Variables:
DO follow a consistent rule for var
and final
on local variables
Use
final
for local variables that are not reassigned andvar
for those that are.Use
var
for all local variables, even ones that aren't reassigned. Never usefinal
for locals. (Usingfinal
for fields and top-level variables is still encouraged, of course.)
AVOID storing what you can calculate
Members:
PREFER using a final
field to make a read-only property
If you have a field that outside code should be able to see but not assign to, a simple solution that works in many cases is to simply mark it final
.
class Box {
final contents = [];
}
CONSIDER using =>
for simple members
double get area => (right - left) * (bottom - top);
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
Constructors:
DO use ;
instead of {}
for empty constructor bodies
class Point {
double x, y;
Point(this.x, this.y);
}
Error Handling:
AVOID catches without on
clauses
- A catch clause with no
on
qualifier catches anything thrown by the code in the try block.
DON'T discard errors from catches without on clauses
- If you really do feel you need to catch everything that can be thrown from a region of code, do something with what you catch. Log it, display it to the user or rethrow it, but do not silently discard it.
DO throw objects that implement Error
only for programmatic errors
DO use rethrow
to rethrow a caught exception
- If you decide to rethrow an exception, prefer using the
rethrow
statement instead of throwing the same exception object usingthrow
.rethrow
preserves the original stack trace of the exception.throw
on the other hand resets the stack trace to the last thrown position.
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
Asynchrony
PREFER async/await over using raw futures
- Asynchronous code is notoriously hard to read and debug, even when using a nice abstraction like futures.
DON'T use async when it has no useful effect
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}
Subscribe to my newsletter
Read articles from Jinali Ghoghari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by