home / skills / kaakati / rails-enterprise-dev / flutter-conventions

This skill applies Dart and Flutter conventions, naming, structure, and async/await best practices to improve code clarity and maintainability.

npx playbooks add skill kaakati/rails-enterprise-dev --skill flutter-conventions

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.9 KB
---
name: "Flutter Conventions & Best Practices"
description: "Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices"
version: "1.0.0"
---

# Flutter Conventions & Best Practices

## Dart 3.x Features

### Pattern Matching
```dart
String describeUser(User user) {
  return switch (user) {
    User(role: 'admin', isActive: true) => 'Active administrator',
    User(role: 'user', isActive: true) => 'Active user',
    User(isActive: false) => 'Inactive account',
    _ => 'Unknown status',
  };
}
```

### Records
```dart
(int, String) getUserInfo() => (123, 'John Doe');

final (id, name) = getUserInfo();
```

### Sealed Classes
```dart
sealed class Result<T> {}
class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}
class Error<T> extends Result<T> {
  final String message;
  Error(this.message);
}
```

## File Naming Conventions

- **Files**: `snake_case.dart`
- **Classes**: `PascalCase`
- **Variables/Functions**: `camelCase`
- **Constants**: `lowerCamelCase` or `SCREAMING_SNAKE_CASE` for compile-time constants

```dart
// user_controller.dart
class UserController extends GetxController {
  static const int maxRetries = 3;
  static const String BASE_URL = 'https://api.example.com';
  
  final userName = 'John'.obs;
  
  void fetchUserData() {
    // ...
  }
}
```

## Directory Organization

**Layer-first** (recommended for Clean Architecture):
```
lib/
├── domain/
├── data/
└── presentation/
```

**Feature-first** (alternative):
```
lib/
└── features/
    ├── auth/
    │   ├── domain/
    │   ├── data/
    │   └── presentation/
    └── profile/
```

## Null Safety

```dart
// Use late for non-nullable fields initialized later
class MyController extends GetxController {
  late final UserRepository repository;
  
  @override
  void onInit() {
    super.onInit();
    repository = Get.find();
  }
}

// Use ? for nullable types
String? userName;

// Use ! only when absolutely certain
final name = userName!; // Use sparingly

// Prefer ?? for defaults
final displayName = userName ?? 'Guest';
```

## Async/Await Best Practices

```dart
// Use async/await for asynchronous operations
Future<User> fetchUser(String id) async {
  try {
    final response = await client.get(Uri.parse('/users/$id'));
    return User.fromJson(jsonDecode(response.body));
  } on SocketException {
    throw NetworkException();
  } catch (e) {
    throw UnknownException(e.toString());
  }
}

// Use Future.wait for parallel operations
Future<void> loadAllData() async {
  final results = await Future.wait([
    fetchUsers(),
    fetchSettings(),
    fetchPreferences(),
  ]);
}

// Use unawaited for fire-and-forget
unawaited(analytics.logEvent('page_view'));
```

## Code Organization Within Files

```dart
class MyClass {
  // 1. Constants
  static const int maxRetries = 3;
  
  // 2. Static fields
  static final instance = MyClass._();
  
  // 3. Instance fields
  final String id;
  final _isLoading = false.obs;
  
  // 4. Constructors
  MyClass(this.id);
  MyClass._();
  
  // 5. Getters/Setters
  bool get isLoading => _isLoading.value;
  
  // 6. Lifecycle methods
  @override
  void onInit() {}
  
  // 7. Public methods
  void publicMethod() {}
  
  // 8. Private methods
  void _privateMethod() {}
}
```

## Widget Best Practices

```dart
// Prefer const constructors
class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return const Text('Hello');
  }
}

// Extract widgets for reusability
class UserCard extends StatelessWidget {
  final User user;
  
  const UserCard({Key? key, required this.user}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: _buildContent(),
    );
  }
  
  Widget _buildContent() {
    return Column(
      children: [
        Text(user.name),
        Text(user.email),
      ],
    );
  }
}
```

Overview

This skill captures Dart 3.x and Flutter 3.x conventions and pragmatic best practices for naming, file layout, null safety, async patterns, and widget/code organization. It condenses idiomatic patterns that reduce runtime errors, improve readability, and scale across feature- or layer-based architectures. Use it to standardize teams and accelerate code reviews and onboarding.

How this skill works

It inspects and prescribes naming conventions (snake_case files, PascalCase classes, camelCase members), directory organization (layer-first and feature-first examples), and file-level ordering (constants, static, fields, constructors, lifecycle, public/private methods). It highlights Dart 3 features like pattern matching, records, and sealed classes and provides concrete guidance for null safety and async/await usage including parallel tasks and fire-and-forget calls.

When to use it

  • When setting up a new Flutter project and choosing a project structure.
  • During code reviews to enforce naming, file layout, and widget rules.
  • When migrating to Dart 3.x features (pattern matching, records, sealed classes).
  • To reduce null-safety bugs and standardize use of late, ?, !, and ?? operators.
  • When optimizing asynchronous flows and parallelizing network/database calls.

Best practices

  • Name files snake_case.dart, classes PascalCase, and use camelCase for variables and functions.
  • Prefer layer-first for Clean Architecture; use feature-first for smaller cross-cutting features.
  • Use late for fields initialized in lifecycle hooks, ? for nullable types, and avoid ! except when provably safe.
  • Favor async/await with try/catch for readable error handling; use Future.wait for parallel calls and unawaited for fire-and-forget.
  • Keep widgets immutable where possible: prefer const constructors and extract reusable widgets.
  • Organize class members consistently: constants, static, instance fields, constructors, getters/setters, lifecycle, public, private.

Example use cases

  • Creating a new app scaffold that enforces filename and directory standards for team consistency.
  • Refactoring controllers to follow null-safety best practices and lifecycle initialization with late fields.
  • Converting multiple sequential network calls into Future.wait to speed startup data loading.
  • Defining domain responses with sealed classes and using pattern matching to simplify state handling.
  • Extracting repeated UI into const widgets and small private builders for performance and testability.

FAQ

When should I use layer-first vs feature-first?

Use layer-first for strict Clean Architecture and large apps that separate domain/data/presentation; choose feature-first when features are independent and you want encapsulated folders per feature.

How do I decide between late and nullable fields?

Use late when a non-nullable field is guaranteed to be initialized before use (e.g., in onInit). Use nullable types when absence is valid and provide defaults with ?? where appropriate.