Flutter & Dart tips
A collection of different tips for Flutter and Dart, mainly some things found during code reviews.
Test files suffix
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
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
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
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 Exception
s.
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
Prefer explicit switch
over if
.
Prefer switch
over if
(or ternary operator) when dealing with enum
s.
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.
enum Option { optionA, optionB, optionC }
String optionToString1(Option option) =>
option == Option.optionA ? 'A' : 'other';
enum Option { optionA, optionB, optionC }
String optionToString2(Option option) {
switch (option) {
case Option.optionA:
return 'A';
default:
return 'other';
}
}
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
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.
void main() {
List<int>? someList = [];
final result = someList?.map((e) => e.toString()) ?? [];
String? someString = 'some string';
if (someString != null && someString.isNotEmpty) {
print(someString);
}
}
void main() {
List<int> someList = [];
final result = someList.map((e) => e.toString());
String someString = 'some string';
if (someString.isNotEmpty) {
print(someString);
}
}