Dart: Idiomatic Efficiency Reference
Table of Contents
- Null Safety
- Collections & Iteration
- Classes & Records
- Async/Await & Streams
- Error Handling
- Flutter-Specific Patterns
- Anti-patterns specific to Dart
1. Null Safety {#nulls}
// ❌ Manual null check
String display;
if (user.name != null) {
display = user.name!;
} else {
display = 'Unknown';
}
// ✅
final display = user.name ?? 'Unknown';
// ❌ Nested null checks
if (user != null && user.address != null && user.address!.city != null) {
print(user.address!.city!);
}
// ✅
final city = user?.address?.city;
if (city != null) print(city);
// ❌ Late field when nullable is correct
late String name; // crashes if accessed before assignment
// ✅ — use late only when you guarantee initialization before access
String? name; // honestly nullable
// late is fine for: late final _controller = TextEditingController();
// ❌ Bang operator (!) everywhere
final name = user.name!;
final city = user.address!.city!;
// ✅ — promote through null checks
final name = user.name;
if (name == null) return;
// name is now non-null (promoted)
2. Collections & Iteration {#collections}
// ❌ Imperative accumulation
final result = <String>[];
for (final item in items) {
if (item.isActive) result.add(item.name.toUpperCase());
}
// ✅
final result = items
.where((i) => i.isActive)
.map((i) => i.name.toUpperCase())
.toList();
// ❌ Manual map construction
final map = <String, List<Item>>{};
for (final item in items) {
map.putIfAbsent(item.category, () => []).add(item);
}
// ✅ (using collection-if/for in a different way — but groupBy isn't built-in)
// The loop above is actually idiomatic Dart. Use package:collection for groupBy:
import 'package:collection/collection.dart';
final map = groupBy(items, (Item i) => i.category);
// ❌ Building list with add() calls
final widgets = <Widget>[];
widgets.add(Header());
if (showSubtitle) widgets.add(Subtitle());
widgets.add(Body());
// ✅ — collection-if
final widgets = [
Header(),
if (showSubtitle) Subtitle(),
Body(),
];
// ❌ Spreading manually
final all = <int>[];
all.addAll(listA);
all.addAll(listB);
// ✅
final all = [...listA, ...listB];
3. Classes & Records {#classes}
// ❌ Manual data class boilerplate
class Point {
final int x, y;
const Point(this.x, this.y);
@override bool operator ==(Object other) => ...
@override int get hashCode => ...
@override String toString() => 'Point($x, $y)';
}
// ✅ (Dart 3.0+)
typedef Point = ({int x, int y});
// or for named class semantics:
class Point {
final int x, y;
const Point(this.x, this.y);
}
// Use package:equatable or Dart records for equality
// ❌ Verbose constructor
class User {
final String name;
final int age;
User(String name, int age) : name = name, age = age;
}
// ✅ — initializing formals
class User {
final String name;
final int age;
const User(this.name, this.age);
}
// ❌ Mutable fields on an immutable object
class Config {
String host;
int port;
Config(this.host, this.port);
}
// ✅
class Config {
final String host;
final int port;
const Config(this.host, this.port);
}
// ❌ Switch on type with if-else chain
if (shape is Circle) {
return (shape as Circle).radius * pi;
} else if (shape is Rectangle) { ... }
// ✅ (Dart 3.0+)
return switch (shape) {
Circle(:final radius) => radius * radius * pi,
Rectangle(:final width, :final height) => width * height,
};
4. Async/Await & Streams {#async}
// ❌ .then() chains
fetchUser()
.then((user) => fetchPosts(user))
.then((posts) => display(posts))
.catchError((e) => log(e));
// ✅
try {
final user = await fetchUser();
final posts = await fetchPosts(user);
display(posts);
} catch (e) {
log(e);
}
// ❌ Sequential await for independent work
final a = await fetchA();
final b = await fetchB();
// ✅
final results = await Future.wait([fetchA(), fetchB()]);
// or with typed destructuring:
final (a, b) = await (fetchA(), fetchB()).wait; // Dart 3.0+ record
// ❌ StreamBuilder doing too much in build
StreamBuilder(
stream: stream,
builder: (ctx, snap) {
if (snap.hasError) return Error();
if (!snap.hasData) return Loading();
final data = snap.data!;
// 50 lines of widget tree...
},
)
// ✅ — extract widget, or use listen + setState for simple cases
5. Error Handling {#errors}
// ❌ Catching Exception (too broad)
try { process(); }
on Exception catch (e) { print(e); }
// ✅ — catch specific types
try {
process();
} on FormatException catch (e) {
throw AppException('Invalid format', cause: e);
} on HttpException catch (e) {
throw AppException('Network error', cause: e);
}
// ❌ Returning null for errors
Future<User?> fetchUser() async {
try { return await api.getUser(); }
catch (_) { return null; } // caller doesn't know why
}
// ✅ — let exceptions propagate, or use sealed Result type
sealed class Result<T> {}
class Success<T> extends Result<T> { final T value; Success(this.value); }
class Failure<T> extends Result<T> { final Object error; Failure(this.error); }
6. Flutter-Specific Patterns {#flutter}
// ❌ Rebuilding entire tree on state change
setState(() {
// changes a single value, but the build() method builds 200 widgets
});
// ✅ — extract subtrees into separate widgets or use ValueListenableBuilder
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (_, value, __) => Text('$value'),
)
// ❌ const-able widget without const
Container(color: Colors.blue)
// ✅
const ColoredBox(color: Colors.blue)
// Mark constructors const when possible; use `const` keyword at call site
// ❌ Navigator.push with MaterialPageRoute everywhere
Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage()));
// ✅ — named routes or GoRouter
context.go('/detail');
7. Anti-patterns specific to Dart {#antipatterns}
| Anti-pattern | Preferred |
|---|
! (bang) operator liberally | null checks and promotion |
dynamic everywhere | proper types |
as cast without check | pattern matching or is check |
| Mutable fields on value objects | final fields |
print() for logging | package:logging or structured logger |
Manual ==/hashCode | records, equatable, or code generation |
setState for complex state | Riverpod / Bloc / Provider |
| Deep widget nesting | extract widgets into classes |
| String-based routing | typed routing (GoRouter) |
late as escape hatch | nullable types or proper initialization |
Future.delayed for debounce | Timer or proper debounce utility |
Limitations
- These are language-specific guidelines and do not cover overall architectural decisions.
- Over-compression might reduce readability; apply judgement.