Skip to main content

Flutter & Dart tips

A collection of different tips for Flutter and Dart, mainly some things found during code reviews.

Test files suffix

tip

Don't forget about _test suffix.

Test files should reside inside a test folder located at the root of your Flutter application or package.

Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.

Don't forget about adding _test suffix to you test files: this is how test runner finds tests. When you’re creating your first test file, it’s easy to spot the mistake, since flutter test will give you an error for no test files found.

But when you already have hundreds of tests, it’s easy to forget about it and end up with “ignored” test. Even during code review we’re not always that watchful about file names.

Beware of firstWhere

caution

firstWhere can throw StateError.

If you don't specify orElse function, and there's no element in collection that satisfies the given predicate, firstWhere function will throw StateError.

Also, you cannot just return null in orElse function, as it doesn't satisfy the function signature.

Instead, use "safer" firstWhereOrNull function from collection package.

void main() {
final list = [1, 2, 4];

// Bad state: no element
final a = list.firstWhere((e) => e == 3);

// Won't compile:
// The return type `Null` isn't a `int`, as required by the closure's context.
final b = list.firstWhere((e) => e == 3, orElse: () => null);

// c == null
final c = list.firstWhereOrNull((e) => e == 3);
}

setUp in tests

tip

Prefer setUp over setUpAll.

Making tests (especially, unit tests) run faster is important. But even more important is to not introduce hidden dependencies between them.

setUpAll makes it very easy to accidentally introduce hidden dependencies between tests that should be isolated.

Avoid using setUpAll / tearDownAll unless absolutely necessary, and prefer using setUp / tearDown.

One way to find hidden dependency is to run tests in random order:

flutter test --test-randomize-ordering-seed random
void main() {
setUpAll(() {
// Set up dependencies that are very slow and unpractical
// to do before every test.
});

setUp(() {
// Everything else.
//
// You can also reset the state of dependencies created in setUpAll
// to avoid hidden test dependencies.
});

test('add', () {
expect(add(1, 2), 3);
});
}

Errors & Exceptions

tip

Don't catch Error.

The difference between Errors and Exceptions is a common source of confusion for Dart/Flutter developers, especially for those coming from other languages.

Exceptions are conditions that you should plan ahead for and catch.

Errors are bugs in your code. You shouldn’t catch them.

That's why, for example, tryEither function, our custom implementation of Try monad, only catches Exceptions.

void main() {
try {
functionThatThrowsException();
} on Exception {
// Do something.
}

try {
functionThatThrowsError();
} on Error {
// Don't do that!
}

// Result will be Left(Exception).
final result = tryEither((_) => functionThatThrowsException());

// Will throw Error.
final result = tryEither((_) => functionThatThrowsError());
}

void functionThatThrowsException() => throw Exception();
void functionThatThrowsError() => throw Error();

switch vs if

tip

Prefer explicit switch over if.

Prefer switch over if (or ternary operator) when dealing with enums.

Even if you're only interested in the subset of options, prefer explicitly mentioning all the branches.

It will be safer to update enum in the future, since compiler will tell you all the places that need to be changed.

Avoid using conditional expressions with enums
enum Option { optionA, optionB, optionC }

String optionToString1(Option option) =>
option == Option.optionA ? 'A' : 'other';
A slightly better version, but still not future-proof
enum Option { optionA, optionB, optionC }

String optionToString2(Option option) {
switch (option) {
case Option.optionA:
return 'A';
default:
return 'other';
}
}
This is the preferable option

You explicitly show your intentions and the compiler will force you to update it whenever enum changes.

enum Option { optionA, optionB, optionC }

String optionToString3(Option option) {
switch (option) {
case Option.optionA:
return 'A';
case Option.optionB:
case Option.optionC:
return 'other';
}
}

Empty strings and lists

tip

Avoid using nullable strings and lists.

Some types (List, String, etc) have a built-in "empty" state – empty string, empty list etc.

Unless you really have a meaningful difference between null and empty String, avoid making these types nullable.

Not only does it make handling them easier, but it's also less confusing for clients, as there will be only one way to define "no value" option.

Bad
void main() {
List<int>? someList = [];
final result = someList?.map((e) => e.toString()) ?? [];

String? someString = 'some string';
if (someString != null && someString.isNotEmpty) {
print(someString);
}
}
Good
void main() {
List<int> someList = [];
final result = someList.map((e) => e.toString());

String someString = 'some string';
if (someString.isNotEmpty) {
print(someString);
}
}