Dart/Flutter Rules
These rules come from app/rules/dart/ in ai-toolkit. They cover
the project's standards for coding style, frameworks, patterns,
security, and testing in Dart/Flutter. Apply them when writing or
reviewing Dart/Flutter code.
Dart Coding Style
Naming
- PascalCase: classes, enums, typedefs, extensions, mixins.
- camelCase: variables, functions, methods, parameters, named constants.
- snake_case: libraries, packages, directories, source files.
- UPPER_SNAKE: not used in Dart. Use camelCase for constants.
- Prefix private members with
_:_internalState,_helper().
Null Safety
- Enable sound null safety (default since Dart 2.12).
- Use
?types only when null is semantically meaningful. - Use
!operator sparingly. Prefer null checks or??fallback. - Use
latekeyword only when initialization is guaranteed before access. - Use
requiredkeyword for mandatory named parameters.
Classes
- Use
constconstructors for immutable classes. - Use factory constructors for caching, subtype selection, or validation.
- Use named constructors for clarity:
Point.fromJson(json). - Use
finalfields for immutable properties. - Use
@immutableannotation on classes that should be immutable.
Functions
- Use named parameters for functions with >2 parameters.
- Use
requiredfor mandatory named parameters. - Use default values for optional parameters.
- Use fat arrow (
=>) for single-expression functions. - Always specify return types for public functions.
Collections
- Use collection literals:
[],{},<String, int>{}. - Use
ifandforinside collection literals for conditional/iterative building. - Use spread operator:
[...list1, ...list2]. - Use
whereType<T>()for type-safe filtering. - Prefer
constcollections when values are known at compile time.
Async
- Use
async/awaitfor all asynchronous operations. - Return
Future<T>from async functions. Never returnvoid. - Use
Stream<T>for continuous data (events, real-time updates). - Use
Future.wait()for concurrent independent operations. - Use
Completer<T>only when wrapping callback-based APIs.
Imports
- Order:
dart:SDK,package:external, relative project imports. - Use
show/hideto limit import scope when names conflict. - Use
asprefix for namespace conflicts:import 'package:foo/foo.dart' as foo. - Prefer relative imports within the same package.
Formatting
- Use
dart format(line length 80) for consistent formatting. - Use
dart analyzefor static analysis with default lint rules. - Use
analysis_options.yamlwith recommended lints:flutter_lintsorlints. - Use trailing commas in multi-line argument lists for cleaner diffs.
Dart Frameworks
Flutter
- Use
StatelessWidgetby default. UseStatefulWidgetonly for local state. - Use
constconstructors andconstwidgets for build optimization. - Use
Keyparameters for widgets in lists for correct diffing. - Extract large
build()methods into smaller widget classes (not methods). - Use
Theme.of(context)andTextThemefor consistent styling.
Navigation
- Use
GoRouterfor declarative, type-safe routing. - Define routes as constants:
static const String home = '/home'. - Use
ShellRoutefor persistent navigation bars across routes. - Use
context.go()for navigation,context.push()for stacking. - Pass arguments via path parameters or
extrafor complex objects.
Networking
- Use
diofor HTTP with interceptors, retry, and cancellation. - Use
retrofit(code gen) for type-safe REST client definitions. - Use interceptors for auth token injection and refresh logic.
- Set timeouts on every request:
connectTimeout,receiveTimeout. - Use
CancelTokenfor cancelling in-flight requests on navigation.
JSON Serialization
- Use
json_serializable(+build_runner) for generatedfromJson/toJson. DefaultfieldRename: FieldRename.noneuses Dart property names as-is — combined with Effective DartlowerCamelCase, this producescamelCaseJSON keys with zero configuration. - Flutter docs recommend: "best if both server and client follow the same naming strategy" (Flutter — JSON and serialization). When they do, no mapping is needed.
- When server uses a different convention, prefer
@JsonSerializable(fieldRename: FieldRename.snake)at the class level (or globally inbuild.yaml) over sprinkling@JsonKey(name:)on every field. Community recommendation from thejson_serializabledocs and pub.dev guides. - Use individual
@JsonKey(name: '...')only for exceptional cases: external API with mixed conventions, reserved Dart keyword collision (class,is,new), or legacy field rename during deprecation window. Document the reason in a comment. - For enum / status / permission values on the wire:
UPPER_SNAKE_CASEis the cross-language community consensus (seecommon/coding-style.md— JSON Wire Format Conventions). Dart enum case names themselves staylowerCamelCaseper Effective Dart; map them to uppercase strings infromJson/toJson(value.toUpperCase()+switch). - Write unit tests asserting both directions (
fromJson+toJson) with explicit expected keys. Catches contract drift at CI time.
Local Storage
- Use
shared_preferencesfor simple key-value persistence. - Use
drift(formerly Moor) for type-safe SQLite with reactive queries. - Use
hivefor fast, lightweight NoSQL local storage. - Use
flutter_secure_storagefor sensitive data (tokens, passwords). - Never store secrets in
shared_preferences(not encrypted).
Dependency Injection
- Use
get_itfor service locator pattern. Register at app startup. - Use
injectable(code gen) for automatic registration from annotations. - Use Riverpod providers as DI containers for testable architecture.
- Register singletons for services, factories for per-use instances.
Platform Channels
- Use
MethodChannelfor invoking native (iOS/Android) code. - Use
EventChannelfor streaming data from native to Dart. - Use
Pigeon(code gen) for type-safe platform channel definitions. - Handle
MissingPluginExceptiongracefully on unsupported platforms.
Testing Frameworks
- Use
flutter_testfor widget tests withWidgetTester. - Use
integration_testpackage for full app integration tests. - Use
patrolfor native-aware integration testing (permissions, notifications). - Use
golden_toolkitfor advanced visual regression testing.
Build and CI
- Use
flutter buildwith--releaseand--dart-definefor env configuration. - Use flavors (
--flavor) for dev/staging/prod build variants. - Use
flutter analyzein CI for static analysis enforcement. - Use
flutter test --coveragewithlcovfor coverage reporting.
Dart Patterns
Error Handling
- Use typed exceptions for domain errors:
class UserNotFoundException implements Exception. - Use
try-catchwith specific exception types. Avoid barecatch (e). - Use
rethrowto preserve stack trace when re-raising exceptions. - Use
Result<T, E>pattern (e.g.,dartzEither) for expected failures. - Use
Future.catchError()only whenasync/awaitis not applicable.
State Management (Flutter)
- Use Riverpod for compile-safe, testable state management.
- Use BLoC pattern for event-driven state with clear input/output.
- Use
ChangeNotifier/ValueNotifierfor simple local state. - Use
StateNotifier(Riverpod) for immutable state transitions. - Keep state classes immutable. Use
copyWith()for updates.
Riverpod
- Use
@riverpodannotation (code gen) for provider definitions. - Use
ref.watch()for reactive dependencies. Useref.read()for one-time access. - Use
AsyncNotifierfor async state management. - Use
autoDisposefor providers that should clean up when unused. - Use
familymodifier for parameterized providers.
BLoC Pattern
- Separate events (input), states (output), and logic (bloc).
- Use
sealed classfor events and states (exhaustiveswitch). - Use
Emitter<State>for emitting state transitions. - Use
transformEvents()for debouncing search inputs. - Use
BlocObserverfor global logging and error tracking.
Repository Pattern
- Abstract data sources behind repository interfaces.
- Repositories return domain models, not DTOs or raw data.
- Use
Future<T>for single values,Stream<T>for real-time updates. - Cache data in repository layer when appropriate.
- Inject repositories via constructor. Use Riverpod/GetIt for DI.
Freezed (Code Generation)
- Use
@freezedfor immutable data classes withcopyWith, equality,toString. - Use
@freezedsealed unions for state modeling:factory State.loading(). - Use
when()/map()for exhaustive pattern matching on freezed unions. - Run
dart run build_runner buildafter modifying freezed classes.
Async Patterns
- Use
Stream.asyncMap()for transforming streams with async operations. - Use
StreamController<T>for custom streams. Close indispose(). - Use
Completer<T>to bridge callback APIs to Future-based APIs. - Use
Timer.periodic()for polling. Cancel indispose(). - Use
compute()(Flutter) for CPU-intensive work on isolates.
Anti-Patterns
- Using
dynamictype: defeats type safety. UseObject?or generics. - Not disposing controllers/subscriptions: causes memory leaks.
- Putting business logic in widgets: extract to services/blocs.
- Using
setState()for global state: use proper state management. - Deep widget nesting: extract sub-widgets as separate classes.
Dart Security
Input Validation
- Validate all user input in form fields with
TextFormFieldvalidators. - Use
RegExpfor pattern validation (email, phone, URL). - Sanitize HTML content before rendering. Never use
Htmlwidget with raw user input. - Validate deep link parameters before navigation or data loading.
- Limit text input length with
maxLengthonTextFormField.
Network Security
- Use HTTPS exclusively. Configure
SecurityContextfor certificate pinning. - Use
diointerceptors for consistent auth header injection. - Validate SSL certificates in production. Do not disable certificate checks.
- Set connection and read timeouts on all HTTP requests.
- Use
CancelTokento abort requests when the user navigates away.
Data Storage
- Use
flutter_secure_storagefor tokens, passwords, and API keys. - Never store sensitive data in
shared_preferences(stored in plaintext). - Encrypt local databases (
driftwithsqlcipher, orhivewith encryption). - Clear secure storage on user logout.
- Use
kIsWebchecks to handle web platform storage limitations.
Authentication
- Use OAuth 2.0 / OIDC with PKCE flow for mobile authentication.
- Store refresh tokens in secure storage. Store access tokens in memory.
- Use
flutter_appauthfor standards-compliant OAuth flows. - Implement biometric authentication with
local_authpackage. - Never store credentials in Dart source code or asset files.
Platform Channel Security
- Validate all data received from native code via platform channels.
- Do not pass sensitive data through
MethodChannellogging-enabled calls. - Use
Pigeonfor type-safe channel communication (prevents mismatched types). - Handle
PlatformExceptiongracefully for missing native implementations.
Obfuscation and Hardening
- Use
--obfuscate --split-debug-info=<dir>for release builds. - Use
--dart-definefor environment-specific configuration (not secrets). - Do not embed API keys in the Dart source. Use server-side proxying.
- Use ProGuard rules (Android) and symbol stripping (iOS) for native code.
WebView Security
- Use
webview_flutterwith JavaScript disabled unless explicitly needed. - Restrict navigation to allowlisted domains with
NavigationDelegate. - Sanitize any data passed from WebView to Dart via JavaScript channels.
- Do not load untrusted URLs in WebViews.
Dependency Security
- Run
dart pub outdatedregularly. Update dependencies promptly. - Audit
pubspec.lockfor unexpected transitive dependencies. - Use
dart pub audit(when available) for vulnerability scanning. - Prefer well-maintained packages with high pub.dev scores.
- Pin exact versions in
pubspec.yamlfor production apps.
Dart Testing
Framework
- Use
package:testfor pure Dart unit tests. - Use
package:flutter_testfor Flutter widget and integration tests. - Use
package:mockitowith@GenerateMocksfor mock generation. - Use
package:mocktailas a simpler alternative (no code generation).
File Naming
- Test files:
foo_test.dartintest/mirroringlib/structure. - Widget tests:
test/widgets/for Flutter widget tests. - Integration tests:
integration_test/directory (Flutter convention). - Golden tests:
test/goldens/for visual regression snapshots.
Structure
- Use
group()for organizing related tests. - Use
setUp()/tearDown()for per-test setup and cleanup. - Use
setUpAll()/tearDownAll()for expensive one-time setup. - Name tests descriptively:
test('returns null when user is not found', ...).
Assertions
- Use
expect(actual, matcher)with built-in matchers. - Use
equals(),isNull,isNotNull,isA<T>()for type/value checks. - Use
throwsA(isA<FormatException>())for exception testing. - Use
completion(expected)for Future assertions. - Use
emitsInOrder([...])for Stream emission testing.
Mocking (Mockito)
- Annotate:
@GenerateMocks([UserRepository]). Runbuild_runner. - Stub:
when(mock.getUser(any)).thenAnswer((_) async => user). - Verify:
verify(mock.saveUser(captureAny)).called(1). - Use
verifyNever()to assert a method was not called. - Use
throwOnMissingStub()to catch unstubbed method calls.
Widget Testing (Flutter)
- Use
testWidgets('description', (tester) async { ... }). - Use
tester.pumpWidget(MaterialApp(home: MyWidget()))to render. - Use
tester.pump()to trigger rebuilds after state changes. - Use
tester.pumpAndSettle()to wait for animations to complete. - Use
find.byType(),find.text(),find.byKey()for widget lookups. - Use
tester.tap(),tester.enterText()for interaction simulation.
Golden Tests
- Use
matchesGoldenFile('goldens/my_widget.png')for visual comparison. - Run
flutter test --update-goldensto regenerate baseline images. - Use golden tests for complex UI components, not simple widgets.
- Keep golden tests platform-specific (render output varies by OS).
Best Practices
- Test public API behavior, not implementation details.
- Use
fakeclasses (implementing interfaces) for simple test doubles. - Use
addTearDown()to register cleanup in the test body. - Run
flutter test --coverageand checkcoverage/lcov.info. - Use
blocTest()frombloc_testpackage for BLoC testing.