agentleFS
FaisalAlqarniFaisalAlqarni/sp-ecc/skills/dart-patterns/SKILL.md

dart-patterns

Skill9 starsChanged 9 months agoMITView on GitHub

Use when writing Dart code - provides idiomatic patterns for null safety, async programming, collections, and error handling

We split this skill into the rules it gives an agent

25 rules. About 0 are common, 9 are distinctive, and 16 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 16

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Prefer non-nullable types by default

    line 53
  2. Very specific:

    Use ? only when null is a valid value

    line 54
  3. Very specific:

    Avoid ! operator; prefer null checks or ??

    line 55
  4. Very specific:

    Use late for two-phase initialization

    line 56
  5. Very specific:

    Use late final for lazy initialization

    line 57
  6. Very specific:

    Use lowerCamelCase for variables, methods, and parameters

    line 759
  7. Very specific:

    Use UpperCamelCase for types (classes, enums, typedefs)

    line 760
  8. Very specific:

    Use lowercase_with_underscores for libraries and file names

    line 761
  9. Very specific:

    Prefer final over var when variable won't be reassigned

    line 762
  10. Very specific:

    Use const for compile-time constants

    line 763
  11. Distinctive:

    Avoid explicit type annotations for local variables when obvious

    line 764
  12. Distinctive:

    Use trailing commas for better formatting

    line 765
  13. Very specific:

    Prefer => for simple single-expression functions

    line 807
  14. Very specific:

    Use if instead of conditional expressions for void returns

    line 808
  15. Very specific:

    Prefer async/await over raw futures

    line 809
  16. Very specific:

    Don't explicitly initialize variables to null

    line 810
  17. Very specific:

    Use interpolation to compose strings: 'Hello, $name!'

    line 811
  18. Very specific:

    Avoid using .length to check if collection is empty.

    line 812
  19. Very specific:

    Use whereType<T>() to filter collections by type

    line 813
  20. Distinctive:

    Prefer function declarations over assigning lambdas to variables

    line 814
  21. Distinctive:

    Use named constructors for clarity

    line 819
  22. Distinctive:

    Prefer composition over inheritance

    line 820
  23. Distinctive:

    Keep classes focused (Single Responsibility)

    line 821
  24. Distinctive:

    Use enums for fixed sets of values

    line 822
  25. Distinctive:

    Avoid these common mistakes:

    line 895

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Dart Patterns

Description

Expert guidance on Dart language idioms, patterns, and best practices for building robust, idiomatic Dart applications.

Trigger

Use when:

  • Writing or reviewing Dart code
  • Working with Flutter or server-side Dart projects
  • Implementing async patterns, null safety, or advanced language features
  • Architecting Dart packages and libraries

Instructions

Core Language Features

Null Safety

Dart's sound null safety system eliminates null reference errors at compile time.

Nullable vs Non-nullable Types:

// Non-nullable (cannot be null)
String name = 'John';
int age = 25;

// Nullable (can be null)
String? nickname;
int? optionalAge;

// Null assertion operator (!) - use sparingly
String definitelyNotNull = nickname!; // Throws if null

// Null-aware operators
String displayName = nickname ?? 'Anonymous'; // Default value
int? length = nickname?.length; // Safe navigation

// Late variables (initialized later, but non-nullable)
late String initLater;
void initialize() {
  initLater = 'Now initialized';
}

// Late lazy initialization
late final String expensive = computeExpensive();

Best Practices:

  • Prefer non-nullable types by default
  • Use ? only when null is a valid value
  • Avoid ! operator; prefer null checks or ??
  • Use late for two-phase initialization
  • Use late final for lazy initialization

Async Programming

Futures:

// Basic Future usage
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Data loaded';
}

// Error handling
Future<String> fetchWithError() async {
  try {
    final result = await riskyOperation();
    return result;
  } on NetworkException catch (e) {
    print('Network error: ${e.message}');
    rethrow;
  } catch (e, stackTrace) {
    print('Error: $e\n$stackTrace');
    return 'default';
  } finally {
    cleanup();
  }
}

// Multiple futures
Future<void> loadMultiple() async {
  // Sequential
  final user = await fetchUser();
  final profile = await fetchProfile(user.id);

  // Parallel
  final results = await Future.wait([
    fetchUser(),
    fetchSettings(),
    fetchNotifications(),
  ]);

  // With error handling per future
  final resultsWithErrors = await Future.wait(
    [fetchUser(), fetchSettings()],
    eagerError: false, // Continue even if one fails
  );
}

// Timeout handling
Future<String> fetchWithTimeout() async {
  try {
    return await fetchData().timeout(
      Duration(seconds: 5),
      onTimeout: () => 'Timed out',
    );
  } on TimeoutException {
    return 'Request timed out';
  }
}

Streams:

// Creating streams
Stream<int> countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

// Stream from iterable
Stream<int> fromList() => Stream.fromIterable([1, 2, 3, 4, 5]);

// Listening to streams
void listenToStream() {
  final subscription = countStream().listen(
    (data) => print('Received: $data'),
    onError: (error) => print('Error: $error'),
    onDone: () => print('Stream closed'),
    cancelOnError: false,
  );

  // Cancel subscription
  subscription.cancel();
}

// Async iteration
Future<void> processStream() async {
  await for (final value in countStream()) {
    print(value);
    if (value == 3) break; // Exit early
  }
}

// Stream transformations
Stream<String> transformStream(Stream<int> input) {
  return input
    .where((n) => n.isEven)
    .map((n) => 'Number: $n')
    .take(10)
    .distinct()
    .handleError((error) => print('Error: $error'));
}

// Broadcast streams (multiple listeners)
Stream<int> createBroadcast() {
  final controller = StreamController<int>.broadcast();

  // Add data
  controller.add(1);
  controller.add(2);

  // Close when done
  controller.close();

  return controller.stream;
}

// Stream controller with error handling
class DataStream {
  final _controller = StreamController<String>();

  Stream<String> get stream => _controller.stream;

  void addData(String data) {
    if (!_controller.isClosed) {
      _controller.add(data);
    }
  }

  void addError(Object error) {
    if (!_controller.isClosed) {
      _controller.addError(error);
    }
  }

  void dispose() {
    _controller.close();
  }
}

Collections

Idiomatic Collection Usage:

// List operations
final numbers = [1, 2, 3, 4, 5];
final doubled = numbers.map((n) => n * 2).toList();
final evens = numbers.where((n) => n.isEven).toList();
final sum = numbers.reduce((a, b) => a + b);
final total = numbers.fold(0, (sum, n) => sum + n);

// List patterns
final [first, second, ...rest] = numbers;
final [head, ...middle, last] = numbers;

// Spread operator
final combined = [...numbers, 6, 7, ...doubled];
final nullSafe = [...?nullableList];

// Set operations
final uniqueNumbers = {1, 2, 3, 2, 1}; // {1, 2, 3}
final union = set1.union(set2);
final intersection = set1.intersection(set2);
final difference = set1.difference(set2);

// Map operations
final userMap = {
  'name': 'John',
  'age': 30,
  'email': 'john@example.com',
};

// Map transformation
final uppercased = userMap.map(
  (key, value) => MapEntry(key.toUpperCase(), value),
);

// Null-aware map access
final email = userMap['email'] ?? 'no-email';

// Collection if/for
final list = [
  1,
  2,
  if (includeThree) 3,
  for (var i in [4, 5, 6]) i * 2,
];

Object-Oriented Patterns

Classes and Constructors:

// Standard class
class User {
  final String id;
  final String name;
  final int age;

  // Named constructor
  User({
    required this.id,
    required this.name,
    required this.age,
  });

  // Factory constructor
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      age: json['age'] as int,
    );
  }

  // Named constructor with defaults
  User.guest()
    : id = 'guest',
      name = 'Guest User',
      age = 0;

  // Copy with method
  User copyWith({
    String? id,
    String? name,
    int? age,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }

  @override
  String toString() => 'User(id: $id, name: $name, age: $age)';

  @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is User &&
    runtimeType == other.runtimeType &&
    id == other.id;

  @override
  int get hashCode => id.hashCode;
}

// Immutable data class pattern
class ImmutableUser {
  const ImmutableUser({
    required this.id,
    required this.name,
  });

  final String id;
  final String name;
}

Mixins:

// Mixin definition
mixin Logging {
  void log(String message) {
    print('[${DateTime.now()}] $message');
  }
}

mixin Validation {
  bool validate(String input) {
    return input.isNotEmpty && input.length >= 3;
  }
}

// Restricted mixin (can only be applied to specific types)
mixin Serializable on Object {
  Map<String, dynamic> toJson();
}

// Using mixins
class UserService with Logging, Validation {
  void createUser(String name) {
    if (validate(name)) {
      log('Creating user: $name');
      // Create user logic
    }
  }
}

Abstract Classes and Interfaces:

// Abstract class
abstract class Animal {
  String get name;
  void makeSound();

  // Concrete method
  void move() {
    print('$name is moving');
  }
}

// Interface pattern (every class is an interface)
class Flyable {
  void fly() {}
}

class Swimmable {
  void swim() {}
}

// Multiple interface implementation
class Duck extends Animal implements Flyable, Swimmable {
  @override
  String get name => 'Duck';

  @override
  void makeSound() => print('Quack');

  @override
  void fly() => print('Duck is flying');

  @override
  void swim() => print('Duck is swimming');
}

Extension Methods:

// String extensions
extension StringExtensions on String {
  bool get isValidEmail {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
  }

  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }

  String truncate(int maxLength) {
    return length <= maxLength ? this : '${substring(0, maxLength)}...';
  }
}

// List extensions
extension ListExtensions<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;
  T? get lastOrNull => isEmpty ? null : last;

  List<T> distinctBy<K>(K Function(T) selector) {
    final seen = <K>{};
    return where((item) => seen.add(selector(item))).toList();
  }
}

// Nullable extensions
extension NullableStringExtensions on String? {
  bool get isNullOrEmpty => this == null || this!.isEmpty;
}

// Usage
void main() {
  final email = 'test@example.com';
  print(email.isValidEmail); // true

  final name = 'john';
  print(name.capitalize()); // John

  final users = [
    User(id: '1', name: 'Alice', age: 25),
    User(id: '1', name: 'Alice', age: 25),
    User(id: '2', name: 'Bob', age: 30),
  ];
  final distinct = users.distinctBy((u) => u.id);
}

Generics

Generic Classes and Methods:

// Generic class
class Box<T> {
  final T value;
  const Box(this.value);

  R transform<R>(R Function(T) transformer) {
    return transformer(value);
  }
}

// Generic with constraints
class Repository<T extends Identifiable> {
  final Map<String, T> _cache = {};

  void save(T item) {
    _cache[item.id] = item;
  }

  T? findById(String id) {
    return _cache[id];
  }

  List<T> findAll() {
    return _cache.values.toList();
  }
}

abstract class Identifiable {
  String get id;
}

// Generic methods
T firstWhere<T>(List<T> list, bool Function(T) predicate, {required T orElse}) {
  for (final item in list) {
    if (predicate(item)) return item;
  }
  return orElse;
}

// Multiple type parameters
class Pair<K, V> {
  final K key;
  final V value;

  const Pair(this.key, this.value);

  @override
  String toString() => '($key, $value)';
}

// Covariant generics
class Animal {}
class Dog extends Animal {}

// This works because List is covariant
List<Animal> animals = <Dog>[];

Error Handling

Exception Patterns:

// Custom exceptions
class NetworkException implements Exception {
  final String message;
  final int? statusCode;

  NetworkException(this.message, [this.statusCode]);

  @override
  String toString() => 'NetworkException: $message (status: $statusCode)';
}

class ValidationException implements Exception {
  final Map<String, String> errors;

  ValidationException(this.errors);

  @override
  String toString() => 'ValidationException: $errors';
}

// Result pattern
class Result<T, E> {
  final T? value;
  final E? error;
  final bool isSuccess;

  const Result.success(this.value)
    : error = null,
      isSuccess = true;

  const Result.failure(this.error)
    : value = null,
      isSuccess = false;

  R when<R>({
    required R Function(T) success,
    required R Function(E) failure,
  }) {
    return isSuccess ? success(value as T) : failure(error as E);
  }
}

// Usage
Future<Result<User, String>> fetchUser(String id) async {
  try {
    final user = await api.getUser(id);
    return Result.success(user);
  } catch (e) {
    return Result.failure('Failed to fetch user: $e');
  }
}

// Option/Maybe pattern
sealed class Option<T> {
  const Option();
}

class Some<T> extends Option<T> {
  final T value;
  const Some(this.value);
}

class None<T> extends Option<T> {
  const None();
}

// Pattern matching with sealed classes
String greet(Option<String> name) {
  return switch (name) {
    Some(value: final n) => 'Hello, $n!',
    None() => 'Hello, stranger!',
  };
}

Package Structure

Standard Project Layout:

my_package/
├── lib/
│   ├── src/           # Private implementation
│   │   ├── models/
│   │   ├── services/
│   │   └── utils/
│   └── my_package.dart # Public API
├── test/
│   ├── src/
│   │   └── models_test.dart
│   └── my_package_test.dart
├── example/
│   └── main.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
└── LICENSE

Public API Pattern (lib/my_package.dart):

library my_package;

// Export public API
export 'src/models/user.dart';
export 'src/services/user_service.dart';

// Hide internal implementation
export 'src/utils/helpers.dart' hide internalHelper;

// Show only specific members
export 'src/utils/validators.dart' show validateEmail, validatePhone;

pubspec.yaml Best Practices:

name: my_package
description: A clear, concise description
version: 1.0.0
homepage: https://github.com/username/my_package

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  # Use version constraints properly
  http: ^1.0.0          # Compatible with 1.x.x
  path: '>=1.8.0 <2.0.0' # Explicit range

dev_dependencies:
  test: ^1.24.0
  lints: ^3.0.0

# Optional: Declare supported platforms
platforms:
  android:
  ios:
  linux:
  macos:
  web:
  windows:

Performance Patterns

Optimization Techniques:

// Const constructors for immutable objects
class Config {
  const Config({
    required this.apiUrl,
    required this.timeout,
  });

  final String apiUrl;
  final Duration timeout;
}

// Use const where possible
const config = Config(
  apiUrl: 'https://api.example.com',
  timeout: Duration(seconds: 30),
);

// Lazy initialization
class ExpensiveResource {
  static ExpensiveResource? _instance;

  factory ExpensiveResource() {
    return _instance ??= ExpensiveResource._internal();
  }

  ExpensiveResource._internal() {
    // Expensive initialization
  }
}

// Efficient string building
String buildLargeString(List<String> parts) {
  final buffer = StringBuffer();
  for (final part in parts) {
    buffer.write(part);
  }
  return buffer.toString();
}

// Avoid unnecessary rebuilds/recomputations
class DataProcessor {
  List<int>? _cachedResult;
  List<int> _lastInput = [];

  List<int> process(List<int> input) {
    // Return cached if input hasn't changed
    if (_cachedResult != null && _listEquals(input, _lastInput)) {
      return _cachedResult!;
    }

    _lastInput = List.from(input);
    _cachedResult = _expensiveComputation(input);
    return _cachedResult!;
  }

  bool _listEquals(List<int> a, List<int> b) {
    if (a.length != b.length) return false;
    for (int i = 0; i < a.length; i++) {
      if (a[i] != b[i]) return false;
    }
    return true;
  }

  List<int> _expensiveComputation(List<int> input) {
    // Heavy computation
    return input.map((x) => x * x).toList();
  }
}

// Isolates for CPU-intensive work
import 'dart:isolate';

Future<int> computeInBackground(int n) async {
  final receivePort = ReceivePort();
  await Isolate.spawn(_heavyComputation, receivePort.sendPort);

  final sendPort = await receivePort.first as SendPort;
  final responsePort = ReceivePort();
  sendPort.send([n, responsePort.sendPort]);

  return await responsePort.first as int;
}

void _heavyComputation(SendPort sendPort) async {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);

  await for (final message in receivePort) {
    final data = message as List;
    final n = data[0] as int;
    final replyPort = data[1] as SendPort;

    // Heavy work
    int result = 0;
    for (int i = 0; i < n; i++) {
      result += i;
    }

    replyPort.send(result);
    break;
  }
}

Effective Dart Guidelines

Style:

  • Use lowerCamelCase for variables, methods, and parameters
  • Use UpperCamelCase for types (classes, enums, typedefs)
  • Use lowercase_with_underscores for libraries and file names
  • Prefer final over var when variable won't be reassigned
  • Use const for compile-time constants
  • Avoid explicit type annotations for local variables when obvious
  • Use trailing commas for better formatting

Documentation:

/// Fetches user data from the API.
///
/// Returns a [User] object if successful, or throws a [NetworkException]
/// if the request fails.
///
/// Example:
/// ```dart
/// final user = await fetchUser('user-123');
/// print(user.name);
/// ```
Future<User> fetchUser(String id) async {
  // Implementation
}

/// Configuration for API client.
///
/// {@template api_config}
/// This class holds all configuration needed to initialize
/// the API client, including base URL and authentication.
/// {@endtemplate}
class ApiConfig {
  /// Creates an API configuration.
  ///
  /// {@macro api_config}
  const ApiConfig({
    required this.baseUrl,
    this.timeout = const Duration(seconds: 30),
  });

  /// The base URL for all API requests.
  final String baseUrl;

  /// Request timeout duration.
  final Duration timeout;
}

Usage:

  • Prefer => for simple single-expression functions
  • Use if instead of conditional expressions for void returns
  • Prefer async/await over raw futures
  • Don't explicitly initialize variables to null
  • Use interpolation to compose strings: 'Hello, $name!'
  • Avoid using .length to check if collection is empty; use .isEmpty
  • Use whereType<T>() to filter collections by type
  • Prefer function declarations over assigning lambdas to variables

Design:

  • Make classes immutable when possible
  • Provide factory constructors for complex object creation
  • Use named constructors for clarity
  • Prefer composition over inheritance
  • Keep classes focused (Single Responsibility)
  • Use enums for fixed sets of values
  • Leverage sealed classes for exhaustive pattern matching

Modern Dart Patterns (3.0+)

Records:

// Record syntax
(int, String) getPair() => (42, 'answer');

// Named fields
({int age, String name}) getUser() => (age: 30, name: 'John');

// Pattern matching with records
final (x, y) = getPoint();
final {:name, :age} = getUser();

// Function returning multiple values
(int min, int max) findRange(List<int> numbers) {
  return (numbers.reduce(min), numbers.reduce(max));
}

Pattern Matching:

// Switch expressions
String describe(Object obj) => switch (obj) {
  int() => 'An integer',
  String() => 'A string',
  List(isEmpty: true) => 'Empty list',
  List(length: 1) => 'List with one element',
  {'key': var value} => 'Map with key: $value',
  _ => 'Something else',
};

// Destructuring
final [first, ...middle, last] = [1, 2, 3, 4, 5];

// If-case
if (value case int x when x > 0) {
  print('Positive integer: $x');
}

Sealed Classes:

sealed class ApiResponse<T> {}

class Success<T> extends ApiResponse<T> {
  final T data;
  Success(this.data);
}

class Error<T> extends ApiResponse<T> {
  final String message;
  Error(this.message);
}

class Loading<T> extends ApiResponse<T> {}

// Exhaustive pattern matching
String handle(ApiResponse<String> response) {
  return switch (response) {
    Success(data: final d) => 'Got data: $d',
    Error(message: final m) => 'Error: $m',
    Loading() => 'Loading...',
    // Compiler ensures all cases are covered
  };
}

Anti-Patterns

Avoid these common mistakes:

  1. Overusing null assertion operator (!)
    // Bad
    String name = user!.profile!.name!;
    
    // Good
    String name = user?.profile?.name ?? 'Unknown';
  2. Not using named parameters for clarity
    // Bad
    createUser('John', 30, true, 'john@example.com');
    
    // Good
    createUser(
      name: 'John',
      age: 30,
      isActive: true,
      email: 'john@example.com',
    );
  3. Ignoring async errors
    // Bad
    fetchData(); // Fire and forget
    
    // Good
    unawaited(fetchData().catchError((e) => log('Error: $e')));
  4. Using dynamic unnecessarily
    // Bad
    dynamic processData(dynamic data) => data;
    
    // Good
    T processData<T>(T data) => data;
  5. Not disposing resources
    // Bad
    class DataService {
      final StreamController _controller = StreamController();
    }
    
    // Good
    class DataService {
      final StreamController _controller = StreamController();
    
      void dispose() {
        _controller.close();
      }
    }

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.