$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

AI Coding Rules for Dart and Flutter

Flutter's widget tree and Dart's type system need specific AI rules — or you get deeply nested builds, mismanaged state, and non-idiomatic patterns.

8 min read·January 28, 2026

Flutter's widget nesting problem is solvable with one rule

Widget extraction, state management, null safety, and architecture patterns

Why Flutter and Dart Need Specific AI Coding Rules

Flutter's declarative UI model and Dart's sound null safety create a unique programming environment. AI assistants struggle with two aspects in particular: the widget tree depth problem (Flutter code becomes deeply nested fast) and state management (Flutter has five popular approaches, and the AI needs to know which one your team uses).

The most common AI failures in Flutter: generating a single build method with 15 levels of nesting, using setState for everything instead of a state management solution, ignoring null safety with excessive null assertions (!), creating monolithic widgets instead of extracting reusable components, and mixing Material and Cupertino widgets inconsistently.

Dart is a well-designed language with strong conventions — the Effective Dart guide is comprehensive. But AI assistants don't follow it consistently without explicit rules in your CLAUDE.md.

Rule 1: Widget Extraction and Composition

The rule: 'Keep build methods under 40 lines. Extract sub-trees into separate widget classes or helper methods when nesting exceeds 4 levels. Every extracted widget should have a descriptive name that explains what it renders, not how: UserProfileCard not UserRowWithAvatarAndNameAndBio. Prefer composition over inheritance — extend StatelessWidget or StatefulWidget, don't create deep widget class hierarchies.'

For reusable widgets: 'Create a widgets/ directory for app-wide reusable widgets. Screen-specific widgets live next to their screen file. Pass data down through constructor parameters — never access global state directly from child widgets. Use const constructors where possible for performance.'

This is the single highest-impact Flutter rule. A 200-line build method with 15 levels of nesting is unreadable and unmaintainable. Extraction makes the code readable, testable, and reusable.

💡 40-Line Rule

Keep build methods under 40 lines. Extract sub-trees into separate widgets at 4+ nesting levels. This one rule makes Flutter code readable, testable, and reusable.

Rule 2: State Management

Flutter's state management landscape is fragmented — setState, Provider, Riverpod, Bloc/Cubit, GetX, MobX. AI assistants pick one at random or default to setState. Your rules must specify which solution your project uses.

For Riverpod: 'Use Riverpod for all state management. Use Provider for read-only values, StateNotifierProvider for mutable state, FutureProvider for async data, StreamProvider for reactive streams. Use ref.watch in build methods, ref.read in callbacks. Never use setState for anything managed by Riverpod.'

For Bloc: 'Use Bloc/Cubit for all state management. Define events as sealed classes, states as sealed classes. Use BlocProvider for dependency injection. Use BlocBuilder for UI updates, BlocListener for side effects (navigation, dialogs). Never access Bloc state directly — always go through BlocBuilder or BlocListener.'

  • Declare your state management solution explicitly: Riverpod, Bloc, or Provider
  • Never use setState for state managed by your chosen solution
  • Riverpod: ref.watch in build, ref.read in callbacks
  • Bloc: sealed events + sealed states, BlocBuilder for UI, BlocListener for effects
  • Keep state classes immutable — use copyWith for updates
⚠️ Declare Your State Solution

Flutter has 5+ state management options. AI picks one at random without rules. Specify 'Use Riverpod' or 'Use Bloc' explicitly — the AI needs to know which one.

Rule 3: Dart Null Safety and Type System

Dart's sound null safety means the compiler guarantees that non-nullable types are never null. AI assistants sometimes bypass this with the ! operator (null assertion) which defeats the safety, or use late for everything instead of proper initialization.

The rule: 'Never use ! (null assertion) except when interfacing with non-null-safe code. Use ? for nullable types, ?? for defaults, ?. for safe access. Avoid late except for dependency injection and framework-required patterns (late final in StatefulWidget). Use required for named parameters that must be provided. Enable strict analysis options: strict-casts, strict-raw-types, strict-inference.'

For generics: 'Use explicit generic types in function signatures. Avoid dynamic — use Object? when the type is genuinely unknown. Use typedef for complex function types. Use sealed class (Dart 3) for exhaustive pattern matching.'

Rule 4: Async Patterns in Dart

The rule: 'Use async/await for all asynchronous operations — never use raw Future.then() chains. Use Stream for reactive data (Firebase, WebSocket, sensor data). Use FutureBuilder and StreamBuilder only at the widget level — never in business logic. In Riverpod, use FutureProvider and StreamProvider instead of builders.'

For error handling: 'Wrap async operations in try/catch at the appropriate level. In state management classes (Notifier, Cubit), catch errors and emit error states. Never let unhandled async errors crash the app — use runZonedGuarded or FlutterError.onError for global error handling.'

For isolates: 'Use Isolate.run() (Dart 2.19+) for CPU-intensive operations like JSON parsing of large payloads, image processing, or complex computations. Never perform heavy computation on the main isolate — it blocks the UI.'

Rule 5: Flutter Architecture Patterns

The rule: 'Use a layered architecture: presentation (widgets, screens) → application (state management, use cases) → domain (models, entities) → data (repositories, API clients). Dependencies point inward — presentation depends on application, never the reverse. Repositories abstract data sources — widgets never call HTTP clients directly.'

For navigation: 'Use GoRouter for declarative routing. Define routes as constants. Use path parameters for resource IDs. Use query parameters for optional filters. Use redirect for auth guards. Never use Navigator.push directly — all navigation goes through the router.'

For project structure: 'Organize by feature, not by type: features/auth/, features/home/, features/settings/. Each feature contains its own screens, widgets, state, and models. Shared code lives in core/ (networking, storage, theme) and shared/ (common widgets, utils).'

  • Layered: presentation → application → domain → data
  • Feature-based structure: features/auth/, features/home/, etc.
  • GoRouter for navigation — declarative routes, path params, auth guards
  • Repositories abstract data sources — widgets never call APIs directly
  • core/ for infrastructure — shared/ for common widgets and utils
Feature-Based Structure

Organize by feature (features/auth/, features/home/), not by type (screens/, widgets/, models/). Each feature is self-contained — easier to navigate, test, and delete.

Complete Flutter/Dart Rules Template

Consolidated template for Flutter teams using Dart 3+.

  • Widget build methods under 40 lines — extract at 4+ nesting levels
  • State management: [Riverpod/Bloc] — never setState for managed state
  • No ! (null assertion) — use ?., ??, required, proper null handling
  • async/await only — no .then() chains, Isolate.run for heavy computation
  • Layered architecture — feature-based directory structure
  • GoRouter for navigation — declarative routes with auth guards
  • const constructors where possible — keys on list items
  • flutter_lints + custom analysis_options.yaml — dart fix for automated fixes