Flutter AI Rules
Overview
A comprehensive, non-opinionated collection of Flutter development rules sourced entirely from official documentation (Flutter, Dart, and major package docs). Designed for AI-powered IDEs (Cursor, Windsurf, etc.) to ensure generated code follows official best practices.
Covers: app architecture, Dart language rules, Dart 3 features, widget composition, state management (ChangeNotifier, Provider, Bloc, Riverpod), testing (Mocktail, Mockito), common errors, and code review.
1. App Architecture
Layered Architecture
- Separate features into a UI Layer (presentation), a Data Layer (business data and logic), and optionally a Domain Layer for complex business logic.
- Organize code by feature (e.g.,
auth/containing viewmodel, use cases, screens) or by type, or use a hybrid approach. - Only allow communication between adjacent layers.
- Define clear responsibilities, boundaries, and interfaces for each layer and component (Views, View Models, Repositories, Services).
UI Layer
- Views describe how to present data; keep logic minimal and UI-related only.
- Pass events from Views to View Models in response to user interactions.
- View Models convert app data into UI state and maintain view-needed state.
- Expose callbacks (commands) from View Models to Views.
Data Layer
- Repositories are the single source of truth (SSOT) for model data; handle caching, error handling, and data refresh.
- Only the SSOT class can mutate its data; all others read from it.
- Repositories transform raw service data into domain models consumed by View Models.
- Services wrap API endpoints, expose async response objects, isolate data-loading, hold no state.
- Use dependency injection for testability and flexibility.
Data Flow
- Follow unidirectional data flow: state flows data -> logic -> UI; events flow UI -> logic -> data.
- Data changes happen in the SSOT only. UI always reflects current immutable state.
Best Practices
- Use MVVM as default pattern; adapt for complexity.
- Use optimistic updates to improve perceived responsiveness.
- Support offline-first by combining local and remote data sources in repositories.
- For small immutable models, prefer
abstract classwithconstconstructors andfinalfields. - Use descriptive constant names (e.g.,
_todoTableNamenot_kTableTodo).
2. Effective Dart
Naming
- Use
UpperCamelCasefor types (classes, enums, typedefs, type parameters, extensions). - Use
lowercase_with_underscoresfor packages, directories, source files, import prefixes. - Use
lowerCamelCasefor other identifiers (variables, parameters). - Capitalize acronyms longer than two letters like words (e.g.,
HttpClient). - Prefer positive form for booleans (
isEnablednotisDisabled).
Types and Functions
- Type annotate variables without initializers and fields where type isn't obvious.
- Annotate return types and parameter types on function declarations.
- Use
Future<void>for async members that produce no values. - Use class modifiers to control extensibility/interface usage.
Style
- Format code using
dart format. - Use curly braces for all flow control statements.
- Prefer
finalovervarwhen values won't change. Useconstfor compile-time constants.
Imports & Structure
- Don't import
srcdirectories of other packages; don't reach into/out oflib. - Prefer relative imports within a package.
- Keep files single-responsibility; limit file length; group related functionality.
- Prefer
finalfields andprivatedeclarations.
Usage
- Use collection literals. Use
whereType()to filter by type. - Use initializing formals. Use
;for empty constructor bodies. - Use
rethrowfor caught exceptions. OverridehashCodewhen overriding==. - Prefer specific exception handling over generic
catch (e).
Documentation
- Use
///doc comments (not block comments). Start with single-sentence summary. - Use square brackets to reference in-scope identifiers.
- Document why code exists, not just what it does. Put doc comments before metadata annotations.
3. Dart 3 Features
Branches
- Use
if-casefor single-pattern matching:
if (pair case [int x, int y]) {
print('Coords: $x, $y');
}
switchstatements/expressions match multiple patterns. Nobreakneeded after match.- Use
sealedclass modifier for exhaustiveness checking on subtypes. - Use
whenguard clauses to further constrain case matching. - Use logical-or patterns (
case a || b) to share bodies between cases.
Patterns
- Patterns match value shapes and destructure into variables. Can be used in declarations, assignments, loops, if-case, switch-case.
- Pattern variable declarations:
var (a, [b, c]) = ('str', [1, 2]); - Swap values:
(b, a) = (a, b); - Object patterns:
var Foo(:one, :two) = myFoo; - JSON validation:
if (data case {'user': [String name, int age]}) {
print('$name is $age');
}
Pattern Types
- Logical-or (
||), logical-and (&&), relational (<,>=, etc.), cast (as), null-check (?), null-assert (!). - List patterns (
[a, b]), map patterns ({"key": v}), record patterns ((a, b: v)), object patterns, wildcards (_). - All pattern types can be nested and combined.
Records
- Anonymous, immutable, aggregate types bundling multiple objects.
- Positional fields accessed via
$1,$2; named fields by name. - Structural typing with automatic
hashCodeand==. - Return multiple values and destructure:
var (name, age) = userInfo(json);
final (:name, :age) = userInfo(json);
- Records for simple data aggregation; classes for abstraction and behavior.
4. Widgets & Performance
- Extract reusable widgets into separate components. Use
StatelessWidgetwhen possible. - Keep
buildmethods simple. Avoid unnecessaryStatefulWidgets. - Use
constconstructors. Avoid expensive operations in build methods. - Implement pagination for large lists. Keep state as local as possible.
5. Common Flutter Errors
- "RenderFlex overflowed" -- Wrap children in
Flexible/Expandedor set constraints. - "Vertical viewport was given unbounded height" -- Give
ListViewinsideColumna bounded height (Expanded,SizedBox). - "InputDecorator...unbounded width" -- Constrain
TextFieldwidth withExpanded/SizedBox. - "setState called during build" -- Don't call
setState/showDialogin build. UseaddPostFrameCallback. - "ScrollController attached to multiple scroll views" -- One controller per scrollable widget.
- "RenderBox was not laid out" -- Check for unbounded constraints in widget tree.
- Use Flutter Inspector to debug layout issues.
6. State Management
ChangeNotifier (Built-in)
- Use
ChangeNotifiermodel classes withnotifyListeners()for state changes.
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
void add(Item item) {
_items.add(item);
notifyListeners();
}
}
- Keep internal state private; expose unmodifiable views.
- Use
ChangeNotifierProviderto provide models. UseConsumer<T>for targeted rebuilds. - Place
Consumerdeep in the tree. UseProvider.of<T>(context, listen: false)for actions without rebuild.
Provider
- Use
MultiProviderto avoid nested providers. context.watch<T>()for reactive listening,context.read<T>()for one-time access,context.select<T, R>()for partial listening.- Don't access providers in
initStateor constructors.
Bloc / Cubit
- Use
Cubitfor simple state;Blocfor event-driven state. Start withCubit, refactor toBlocas needed. - Name events in past tense (
LoginButtonPressed). Name states as nouns (LoginSuccess,LoginFailure). - Extend
Equatablefor states. Use@immutable. ImplementcopyWith. BlocBuilderfor rebuilds,BlocListenerfor side effects,BlocConsumerfor both.- Inject repositories via constructors. Avoid direct bloc-to-bloc communication.
- Use
bloc_lintto enforce best practices.
Riverpod
- Wrap app root with
ProviderScope. Define providers asfinalat top level. ref.watchfor reactive listening,ref.readfor one-time,ref.listenfor imperative subscriptions.
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(myProvider);
return Text('$value');
}
}
- Enable
autoDisposefor parameterized providers to prevent memory leaks. - Use Notifiers (
Notifier,AsyncNotifier) for side effects. Use.familyfor parameterized providers. - Install
riverpod_lintfor IDE refactoring and best practices.
7. Testing
General Principles
- Write unit tests for business logic, widget tests for UI.
- Ask: "Can this test actually fail if the real code is broken?"
- Always use
group()named after the class under test. - Name tests with "should":
test('value should start at 0', () {...}).
Mocktail
- Use
Fakefor lightweight implementations;Mockfor interaction verification. registerFallbackValuefor custom types in argument matchers.when(() => mock.method()).thenReturn(value)for stubbing;verifyfor interaction checks.- Prefer real objects > tested fakes > mocks.
Mockito
@GenerateMocks/@GenerateNiceMocks+dart run build_runner build.when(mock.method()).thenReturn(value)for stubs;thenAnswerfor runtime responses.captureAny/captureThatfor argument capture. Don't mock data models.
Bloc Testing
- Use
bloc_testpackage withblocTestfunction:
blocTest<CounterBloc, int>(
'emits [1] when increment is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterIncrementPressed()),
expect: () => [1],
);
Riverpod Testing
- Create new
ProviderContainer(unit) orProviderScope(widget) per test. - Use
overridesto inject mocks/fakes. Mock dependencies, not Notifiers.
8. Code Review
- Verify branch is feature/bugfix/PR, not main/develop. Check branch is up-to-date.
- For each file: correct directory, naming conventions, clear responsibility.
- Review readability, logic correctness, edge cases, modularity, error handling.
- Check security (input validation, no secrets), performance, documentation, test coverage.
- Verify change set is focused on stated purpose with no unrelated changes.
- Be objective; use devil's advocate approach for honest feedback.
Appendix: Firebase Integration (Reference)
The source repository includes detailed Firebase rules for: Cloud Firestore, Firebase Auth, Realtime Database, Cloud Functions, Firebase Storage, Messaging (FCM), Crashlytics, Analytics, App Check, Remote Config, In-App Messaging, Data Connect, Firebase AI, and FlutterFire CLI configuration (multi-flavor support).
See rules/firebase/ in the original repository for full details.