Effective Dart: Usage

Jinali GhoghariJinali Ghoghari
6 min read

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 under libis 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 imports api.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 and var for those that are.

  • Use var for all local variables, even ones that aren't reassigned. Never use final for locals. (Using final 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 using throw. 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]);
}
0
Subscribe to my newsletter

Read articles from Jinali Ghoghari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jinali Ghoghari
Jinali Ghoghari