--- name: dart description: "Language-specific super-code guidelines for dart." risk: safe source: community date_added: "2026-06-16" --- # Dart: Idiomatic Efficiency Reference ## Table of Contents 1. [Null Safety](#nulls) 2. [Collections & Iteration](#collections) 3. [Classes & Records](#classes) 4. [Async/Await & Streams](#async) 5. [Error Handling](#errors) 6. [Flutter-Specific Patterns](#flutter) 7. [Anti-patterns specific to Dart](#antipatterns) --- ## 1. Null Safety {#nulls} ```dart // ❌ Manual null check String display; if (user.name != null) { display = user.name!; } else { display = 'Unknown'; } // ✅ final display = user.name ?? 'Unknown'; ``` ```dart // ❌ 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); ``` ```dart // ❌ 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(); ``` ```dart // ❌ 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} ```dart // ❌ Imperative accumulation final result = []; 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(); ``` ```dart // ❌ Manual map construction final map = >{}; 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); ``` ```dart // ❌ Building list with add() calls final widgets = []; widgets.add(Header()); if (showSubtitle) widgets.add(Subtitle()); widgets.add(Body()); // ✅ — collection-if final widgets = [ Header(), if (showSubtitle) Subtitle(), Body(), ]; ``` ```dart // ❌ Spreading manually final all = []; all.addAll(listA); all.addAll(listB); // ✅ final all = [...listA, ...listB]; ``` --- ## 3. Classes & Records {#classes} ```dart // ❌ 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 ``` ```dart // ❌ 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); } ``` ```dart // ❌ 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); } ``` ```dart // ❌ 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} ```dart // ❌ .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); } ``` ```dart // ❌ 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 ``` ```dart // ❌ 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} ```dart // ❌ 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); } ``` ```dart // ❌ Returning null for errors Future 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 {} class Success extends Result { final T value; Success(this.value); } class Failure extends Result { final Object error; Failure(this.error); } ``` --- ## 6. Flutter-Specific Patterns {#flutter} ```dart // ❌ 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( valueListenable: counter, builder: (_, value, __) => Text('$value'), ) ``` ```dart // ❌ 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 ``` ```dart // ❌ 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.