Naming Conventions
Consistent, role-carrying names make code searchable and self-explaining, and the suffix on a type declares which layer it lives in — so a reviewer, a grep, and a banned-import gate can all read the layer off the name alone. This skill is the how; the normative what is Effective Dart. Never invent a house style that contradicts the language's own.
Non-negotiable rules
- Types are
UpperCamelCase. Classes, enums, mixins, extensions, typedefs, type parameters:TaskScreen,OrderStatus,Predicate<T>. Consistent shape makes types visually distinct from values. - Members, variables, functions, and parameters are
lowerCamelCase.dueDate,loadTasks(),itemCount. It is the language default; deviating costs readers a double-take. - Constants are
lowerCamelCase, neverSCREAMING_CAPS.const maxItemsPerPage = 50;— notconst MAX_ITEMS = 50. Dart dropped the C convention; the analyzer expectsconstant_identifier_names. - Files, folders, libraries, and import prefixes are
lowercase_with_underscores.task_detail_screen.dart,features/task_detail/,import 'package:app_core/app_core.dart';. Cross-platform filesystems and pub demand it. - File name = its primary declaration, snake_cased, one primary public type per file.
TaskNotifierlives intask_notifier.dart. Noutils.dart/helpers.dart/models.dartgrab-bags and noutils//common//helpers//misc/junk-drawer folders — a reader who greps a symbol must land in the file that owns it.core/is the sanctioned pure-foundation layer (value objects,Result/Failure, theClockseam, pure calculators), not a junk-drawer — seeproject-structure-and-packages, which owns the layout. - Acronyms longer than two letters are cased like a word.
Json,Http,Url,Api→JsonOrder,HttpClient,fromJson,imageUrl— notJSONOrder,HTTPClient. Two-letter caps-in-English acronyms may stay caps as types (ID,UI). Mixed-case acronyms are unsearchable and inconsistent. - A leading underscore means library-private — use it only when you mean private. Never prefix a public symbol with
_to "namespace" it; that makes it unusable from another file. Public (no_) is a documented contract — seedartdoc-conventions. - No Hungarian / type-encoding in names. Not
strName,iCount,lstItems,userMap,nameString,itemsList. The type system already knows the type; writename,usersById,items. - Full dictionary words; units and semantics live in the name.
maxItemsPerPage,retryDelaySeconds,orderTotalMinorUnits— never baremax,delay,total. Abbreviations (opt,qty,amt) are confined to the inside of one short pure function with a comment mapping them. A name that omits its unit invites a unit bug. - Booleans read as assertions.
isLoading,hasError,canSubmit,shouldRetry— notloading,error,retry. Boolean getters and methods startis/has/can/shouldso a condition reads like prose. - No
get-prefixed accessors. ExposedueTasks, notgetDueTasks(). Dart has real getters. Functions are verb phrases (loadTasks(),scheduleReminder()); non-boolean getters are noun phrases (itemCount,nextDueDate). - Imports grouped and sorted:
dart:first, thenpackage:, then relative — each group alphabetized,exports in their own section after imports. Letdart formatplus thedirectives_orderinglint enforce it; never hand-fight the formatter.
The suffix declares the layer
A role suffix turns a name into a layer contract: a grep or a path-scoped import gate can tell a repository from a view from a platform boundary without opening the file. Use neutral domain nouns (Task, Order, Account, Item) for the entity token.
| Role | Suffix / pattern | Layer | File → symbol |
|---|---|---|---|
| Screen / route target | [Feature]Screen |
UI (View) | task_list_screen.dart → TaskListScreen |
| Reusable widget | [Thing] (+ Widget only if ambiguous) |
UI | task_card.dart → TaskCard |
| ViewModel (Riverpod, default) | [Feature]Notifier |
UI (ViewModel) | task_list_notifier.dart → TaskListNotifier |
| ViewModel (Provider appendix only) | [Feature]ViewModel |
UI (ViewModel) | task_list_view_model.dart → TaskListViewModel |
| Repository (interface) | [Entity]Repository |
data | task_repository.dart → TaskRepository |
| Repository (impl) | Drift[Entity]Repository / Remote[Entity]Repository |
data | drift_task_repository.dart |
| Drift DAO | [Entity]Dao |
data | tasks_dao.dart → TasksDao |
| Capability interface you define | [Concern]Service |
boundary | share_service.dart → ShareService |
| Service impl (per flavor) | [Provider][Concern]Service |
app adapter | firebase_analytics_service.dart → FirebaseAnalyticsService |
| Wrapper over a specific plugin/SDK/native channel | [Concern]Gateway |
boundary | notification_gateway.dart → NotificationGateway |
| Gateway impl | [Plugin][Concern]Gateway / Live[Concern]Gateway |
app adapter | fln_notification_gateway.dart → FlnNotificationGateway |
| Pure-Dart domain logic | [Domain] + verb suffix (Calculator/Validator/Formatter) |
domain | price_calculator.dart → PriceCalculator |
| Immutable model | domain noun, no suffix | any | task.dart → Task |
| Failure type | [Domain]Failure (sealed) |
any | order_failure.dart → OrderFailure |
Notifieris the ViewModel role — neverViewModelorChangeNotifierin the type name. On Riverpod 3.x the ViewModel is aNotifier/AsyncNotifier/StreamNotifier, one perScreen, 1:1, named[Feature]Notifierin[feature]_notifier.dart(file = primary declaration).[Feature]ViewModelin[feature]_view_model.dartis sanctioned ONLY in the Provider/ChangeNotifierappendix — never on the Riverpod path, and never[Feature]Controller. Seestate-management-riverpod.ServicevsGateway— both name provider-free boundary ports, distinguished by who owns the contract. A[Concern]Serviceis a capability interface YOU define (ShareService,AnalyticsService); a[Concern]Gatewayis the thin wrapper over a SPECIFIC external plugin/SDK or nativeMethodChannel(NotificationGatewayoverflutter_local_notifications,SecureStorageGatewayoverflutter_secure_storage). Domain and UI code names the interface; the concrete impl (FirebaseAnalyticsService,FlnNotificationGateway) lives only in the composition root. A concrete impl name leaking into shared code is a grep-catchable smell. Seeservice-boundary-and-native.- Pure-Dart types carry a
Calculator/Validator/Formatter/Parserverb suffix and touch no Flutter. The suffix advertises that the type is framework-free and unit-testable without a widget. - Match the domain's ubiquitous language and keep it consistent everywhere — one word per concept across models, repositories, and UI, so search finds every reference.
Value vs. instant, and other name-carried distinctions
Encode a semantic distinction the type system can't in the name. A raw instant (DateTime.now()) used where a stable calendar value is expected is a common correctness bug; naming makes the boundary visible.
// A real wall-clock instant is legal only at the Clock boundary and named as one.
final DateTime capturedAt = clock.now(); // instant, from package:clock's Clock (never DateTime.now())
// A stored quantity carries its canonical unit in the name.
final int priceMinorUnits; // not `price` — unit is explicit
final int distanceMeters; // SI base unit, integer
// Unused callback params are `_` (and `__` for a second).
onChanged: (_) => notifier.refresh(),
See value-objects-money-and-units for canonical storage and service-boundary-and-native for the Clock (from package:clock) injected via clockProvider — never a bespoke ClockService.
Worked example
// task_list_notifier.dart — file name == primary declaration, snake_cased
import 'dart:async'; // dart: group, alphabetized
import 'package:flutter_riverpod/flutter_riverpod.dart'; // package: group, alphabetized
import 'task.dart'; // relative group, alphabetized
/// Frozen paging bound — unit lives in the name (rule 9).
const int maxItemsPerPage = 50; // lowerCamelCase const, not SCREAMING_CAPS
/// ViewModel for [TaskListScreen]. One Notifier per Screen, 1:1 (role suffix).
class TaskListNotifier extends AsyncNotifier<List<Task>> {
@override
Future<List<Task>> build() => ref.watch(taskRepositoryProvider).loadTasks();
/// Verb-phrase command; routes through the repository's single write path.
Future<void> archive(String taskId) async {
await ref.read(taskRepositoryProvider).archive(taskId);
ref.invalidateSelf();
}
bool get hasTasks => state.valueOrNull?.isNotEmpty ?? false; // boolean assertion
int get taskCount => state.valueOrNull?.length ?? 0; // noun getter, no get- prefix
}
Anti-patterns
SCREAMING_CAPSconstants in new Dart (const MAX_ITEMS = 50) — uselowerCamelCase; the analyzer flags it.- Class name ≠ file name (
TaskScreenliving inhome.dart), or a grab-bagutils.dart/models.dartholding unrelated things — breaks grep-to-file. - Naming a Flutter-touching type
…Calculator/…Validator(a pure-Dart suffix), or a Riverpod ViewModel…ViewModel/…VM/…Controller— the suffix then lies about the layer (…Notifieron the Riverpod path;…ViewModelonly in the Provider appendix). - A concrete impl name (
FlnNotificationGateway,FirebaseAnalyticsService) referenced from shared/domain code — name theGateway/Serviceinterface; the impl belongs in the composition root. DateTime.now()captured into a field meant to be stable, or an unnamed instant crossing into domain logic — inject aClockand name the instant (capturedAt).getDueTasks()/get/setprefixes — Dart has real getters.- Type baked into a name:
userMap,nameString,itemsList,strName,iCount. - Booleans without
is/has/can/should(loading,valid); unit-silent quantities (total,delay). - Leading
_to "namespace" a public symbol (it makes it private and unreachable);l/O/Isingle letters ordata/temp/fooin committed code. - Abbreviations (
opt,qty,msg) outside a single short function with a mapping comment. - Hand-sorted or mixed import groups — run
dart format; obeydirectives_ordering.
Definition of done
- Types
UpperCamelCase; members/vars/constantslowerCamelCase; files/folders/prefixeslower_snake_case. - File name matches its primary declaration; one primary public type per file; no junk-drawer file or folder.
- Architectural suffix applied and correct for the layer (
Screen/Notifier/Repository/Dao/Service/Gateway/Failure). - Riverpod ViewModels are
…Notifier(not…Controller/…ViewModel/…VM); pure-Dart types carry aCalculator/Validator/Formattersuffix and no Flutter import;Service/Gatewayinterfaces are provider-free with impls confined to the composition root. - Acronyms >2 letters word-cased; full dictionary words; units + semantics in the name; instants named at the
Clockboundary. - Booleans read as assertions; functions are verb phrases; getters are noun phrases with no
getprefix. - Imports grouped
dart:/package:/relative and alphabetized;dart format+dart analyze --fatal-infosclean.
Related skills
dart3-idioms-and-coding-standards— which construct (sealed/record/enum) each declaration earns, and complexity limits.state-management-riverpod— why the ViewModel is aNotifier, and the single write path.service-boundary-and-native— provider-freeServiceinterfaces and their composition-root impls.value-objects-money-and-units— canonical-unit storage behind the unit-in-name rule.namingneighbors:dartdoc-conventionsfor the///contract on public symbols,lint-and-style-configfor thedirectives_ordering/constant_identifier_namesenforcement, andflutter-architecturefor the layer DAG the suffixes map to.
References
- Effective Dart: Style — casing, file names, import ordering (normative).
- Effective Dart: Design — naming — booleans, getters vs. methods, verb/noun phrasing.
- Dart linter rules —
constant_identifier_names,camel_case_types,file_names,directives_ordering,non_constant_identifier_names.