station · ivanmisic.net log in
§02 flutter-rules ↵ back to rules
Mine Rules

flutter-rules

A Flutter / Dart .claude/rules pack: null-safety and idiom standards, state-management principles (Riverpod), and router-agnostic navigation discipline (GoRouter).

The Flutter and Dart layer that sits on top of the generic pack. Drawn from a production Flutter app on Riverpod and GoRouter, generalized so the discipline holds even if your packages differ.

What's inside

  • dart-standards.md: null-safety discipline, naming, const/final idioms, async and subscription cleanup, collection and import order, docs and lint.
  • state-management.md: logic out of widgets, the repository pattern, reading state correctly (read vs watch vs listen), the data/loading/error branch, and testing via overrides. Riverpod examples, portable principles.
  • navigation.md: route constants over hardcoded strings, push-vs-switch, sheet-vs-full-screen, redirect-based auth guards, and treating deep-link parameters as untrusted input.

How to use

Download and drop the .md files into .claude/rules/ alongside the generic pack. They load whenever you touch a .dart file. If you use Bloc or a different router, keep the principles and adjust the package-specific examples.

Want these tuned to your actual codebase? Run /a-rules-optimizer from the dev-workflow-forge plugin.

What's in the bundle 3 files
dart-standards.md 1.4 KB
---
paths:
  - "**/*.dart"
---

# Dart Standards

Layers on the generic code-quality and architecture rules.

## Null Safety
- Non-nullable by default. Add `?` only where null is a real state for the value.
- Use `!` only when you can prove the value is non-null; prefer `??` and `?.`.
- Use `late` sparingly, only for a value guaranteed to be set before its first read.

## Naming
- Classes `PascalCase`, files `snake_case.dart`, variables and functions `camelCase`, private members `_leadingUnderscore`, global constants `SCREAMING_SNAKE_CASE`.

## Idioms
- `const` constructors wherever possible; `final` over `var`.
- Named parameters for anything with more than one or two arguments.
- Keep widgets small and focused. Extract a method rather than nesting deeply.
- Extension methods for utilities rather than free-floating helpers.

## Async
- `async`/`await` over raw `Future` chains. Wrap awaited calls in try-catch.
- `Future.wait` for genuinely parallel work.
- Cancel stream subscriptions in `dispose()`. A live subscription after dispose is a leak and a crash.

## Collections and Imports
- Collection literals (`[]`, `{}`), the spread operator, and `for`/`if` inside collection literals.
- Import order: `dart:`, `package:`, then relative. Use `show`/`hide` to keep imports narrow.

## Docs and Lint
- `///` doc comments on public APIs.
- Follow `flutter_lints` (or your `analysis_options.yaml`). Fix analyzer warnings; don't suppress them without a reason.
navigation.md 1.1 KB
---
paths:
  - "**/*.dart"
---

# Navigation

The examples use GoRouter (the stack this pack is drawn from); the discipline is router-agnostic.

## Route Constants
- Define every route path as a constant in one place. Never hardcode a route string at a call site.

```dart
context.push(AppRoutes.settings); // correct
context.push('/announcements');         // wrong, use the constant
```

## Push vs Switch
- Switch between top-level tabs with `go`; push a screen outside the tab shell with `push`. Be deliberate about which one hides the bottom navigation.

## Sheet vs Full Screen
- Bottom sheet for detail views, forms, pickers, and confirmations.
- Full screen for list views and complex editors.

## Auth Guard
- Gate protected routes with a redirect at the router level, not with a check scattered through each screen. Unauthenticated goes to the login route.

## Deep Links
- Scope your custom URL scheme to the specific callbacks it exists for (an OAuth return, a specific entity), not arbitrary routes.
- Validate every parameter that arrives through a deep link before you act on it. A deep link is untrusted input.
state-management.md 1.6 KB
---
paths:
  - "**/*.dart"
---

# State Management

The examples use Riverpod (the stack this pack is drawn from), but the principles hold for Bloc or Provider too. The point is where state and logic live, not which package holds them.

## Principles
- Business logic lives in a notifier or repository layer, never in a widget's `build`.
- No global mutable state outside your state containers.
- Dispose resources. Lean on the framework's automatic disposal of unused state where it exists, and clean up anything it doesn't.
- Keep each unit of state small and focused.

## Reading State (Riverpod terms)
- Read once in callbacks and event handlers (`ref.read`); subscribe and rebuild in `build` (`ref.watch`); side effects without a rebuild via `ref.listen`.
- Don't create providers inside `build`. Declare them at top level.
- Use `select` to rebuild only on the slice you care about, and `family` for parameterized providers.

## Repository Pattern
- A repository wraps the data source; a provider exposes it. Screens depend on the provider, not the raw client.

```dart
final userRepositoryProvider = Provider((ref) => UserRepository(ref.watch(apiClientProvider)));
final usersProvider = FutureProvider((ref) => ref.watch(userRepositoryProvider).getUsers());
```

## Async States
- Branch on the async value for every data view: data, loading, and error, each handled. No screen that renders blank while a future is pending.

```dart
usersProvider.when(data: (u) => UserList(u), loading: () => const Spinner(), error: (e, st) => ErrorView(e));
```

## Testing
- Override providers inside a scope for tests instead of reaching for real network calls.

Everything mine on this shelf comes from my own working setup, shared for learning. Test it and adapt it to your project before relying on it; you run it at your own risk.