Flutter + Dart AI Rules

Rules for Flutter apps: stateless-first widget design, Riverpod for state, freezed for models, navigation with go_router, and platform-channel patterns. Targets current null-safe Dart 3.

DartFlutter#flutter#dart#riverpod#freezedLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

Flutter + Dart

Project context

This is a Flutter app targeting iOS and Android (with optional web). We use Riverpod for state management, freezed for immutable models, go_router for navigation, and Dart 3 with sound null safety throughout.

Stack

  • Dart 3.4+
  • Flutter 3.22+ (stable channel)
  • Riverpod 2.x for state management (flutter_riverpod)
  • freezed + json_serializable for models
  • go_router for navigation
  • dio (or http) for HTTP
  • Material 3 theming
  • flutter_test + integration_test for tests
  • Mocktail for mocks

Folder structure

lib/
  main.dart
  app.dart                  — root MaterialApp.router
  config/
    theme.dart
    env.dart
  router/
    app_router.dart         — go_router config
  features/
    auth/
      data/
      domain/
      presentation/
        controllers/
        widgets/
        screens/
    posts/
      data/
        post_repository.dart
        post_api.dart
      domain/
        post.dart           — freezed model
      presentation/
        controllers/
        widgets/
        screens/
  shared/
    widgets/
    extensions/
test/
integration_test/

One folder per feature. Each feature has data/ (repos, APIs), domain/ (models, pure logic), presentation/ (UI + controllers).

Widgets

  • StatelessWidget when possible; StatefulWidget only when truly needed
  • Prefer ConsumerWidget / HookConsumerWidget when the widget reads providers
  • Pass values down via constructor; use .copyWith instead of mutation
  • Extract any widget over ~80 lines into its own file
class PostList extends ConsumerWidget {
  const PostList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final posts = ref.watch(postsProvider);
    return posts.when(
      data: (list) => ListView.builder(
        itemCount: list.length,
        itemBuilder: (_, i) => PostTile(post: list[i]),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Error: $e')),
    );
  }
}

State management with Riverpod

  • One provider per piece of state
  • Use Notifier / AsyncNotifier (the modern API) — not StateNotifier (legacy)
  • Use code-generation: @riverpod annotation + build_runner
  • Keep providers small; compose via ref.watch(...)
@riverpod
class Posts extends _$Posts {
  @override
  Future<List<Post>> build() async {
    final repo = ref.watch(postRepositoryProvider);
    return repo.list();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => ref.read(postRepositoryProvider).list());
  }
}

Models with freezed

  • All domain models are freezed unions or data classes
  • Use @freezed + fromJson / toJson via json_serializable
  • Use sealed unions for state machines and result types
@freezed
class Post with _$Post {
  const factory Post({
    required String id,
    required String title,
    required String body,
    required DateTime createdAt,
  }) = _Post;

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}

After editing freezed models: dart run build_runner build --delete-conflicting-outputs

Navigation

  • go_router for declarative routing
  • Define routes in one place: app_router.dart
  • Use typed routes (GoRouteData + routes annotation) for compile-time route safety
  • Use context.go(...) for replace; context.push(...) for stack push
  • Never Navigator.of(context).push(...) — bypasses go_router's URL state

Async and error handling

  • AsyncValue<T> from Riverpod — use .when(data:, loading:, error:)
  • Don't .then((v) {}) — use await always
  • Network errors at the data layer; UI catches at the controller / provider level
  • Never try { ... } catch (e) {} empty — always re-throw or log

Theming

  • Define one ThemeData in config/theme.dart
  • Use Material 3 (useMaterial3: true) — it's the default in Flutter 3.16+
  • Define ColorScheme.fromSeed(seedColor: ...) for both light and dark
  • Use Theme.of(context).colorScheme.primary — never raw hex in widgets

Patterns to follow

  • const constructors everywhere possible — Flutter rebuilds aggressively; const helps
  • Extract magic numbers to a Sizes / Spacing class
  • Use Form + TextFormField + validator — not custom error tracking
  • Localize strings with flutter_localizations from day one if i18n is on the roadmap

Patterns to avoid

  • StatefulWidget for state that should be in a provider
  • setState for cross-screen state — use Riverpod
  • Calling ref.read in build — use ref.watch; ref.read is for callbacks
  • Mutating freezed instances — use .copyWith
  • Hardcoded colors / sizes in widgets — use theme tokens / spacing constants
  • print() for debugging — use debugPrint or a proper logger
  • Long methods in build — extract widgets

Testing

  • Widget tests for screens (testWidgets(...))
  • Unit tests for controllers and repos
  • Integration tests for critical user flows (integration_test/ folder)
  • Use Mocktail for mocking; never mock concrete classes — mock abstractions
  • flutter test --coverage for coverage; aim for meaningful coverage on logic, not widgets

Tooling

  • flutter run — runs on connected device / simulator
  • flutter build apk --release / flutter build ipa --release
  • dart run build_runner watch --delete-conflicting-outputs — for freezed / riverpod / json codegen
  • flutter test
  • dart format .
  • flutter analyze

AI behavioral rules

  • Default to Riverpod's @riverpod Notifier API; never StateNotifier (legacy)
  • All models are freezed — never plain Dart classes for data
  • After editing freezed / riverpod / json_serializable code, run build_runner build (don't hand-edit .g.dart / .freezed.dart)
  • Always add const constructors where possible
  • Use ref.watch in build; ref.read only in callbacks
  • Use context.go / context.push, never Navigator directly
  • Never mutate freezed instances; always .copyWith
  • Run flutter analyze and flutter test before declaring a task done

Frequently asked

How do I use this Flutter rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these Flutter rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.