Skip to main content

Naming conventions

Consistent names are what make the Screen / State / View split navigable. When every screen follows the same scheme, finding the State for a View - or the hook for a State - is mechanical. These are the rules the codebase holds to.

The four parts of a screen

A screen named <Feature> produces four symbols. Derive all of them from the feature name:

PartNameExample
State class<Feature>StateCheckoutScreenState
State hookuse<Feature>StateuseCheckoutScreenState
View<Feature>ViewCheckoutScreenView
Screen (coordinator)<Feature>Screen / <Feature>PageCheckoutScreen
// DON'T - unrelated names; the View and its State are not obviously a pair
class CheckoutData { /* ... */ }
CheckoutData useCheckoutLogic() { /* ... */ }
class CheckoutWidget extends StatelessWidget { /* ... */ }

// DO - one feature name threaded through all four
class CheckoutScreenState { /* ... */ }
CheckoutScreenState useCheckoutScreenState() { /* ... */ }
class CheckoutScreenView extends StatelessWidget { /* ... */ }
class CheckoutScreen extends HookWidget { /* ... */ }

Hooks start with use

Every hook - built-in, custom, screen-level, or global - starts with use. The prefix is what marks a function as obeying the rules of hooks (stable call order, no conditionals). A function that returns state but is not itself a hook should not borrow the prefix.

// DON'T
CheckoutScreenState buildCheckoutState() { /* calls useState, useProvided... */ }
CheckoutScreenState checkoutState() { /* ... */ }

// DO
CheckoutScreenState useCheckoutScreenState() { /* ... */ }

The class name matches the file basename

The top-level class in a .dart file is the file's basename, PascalCased; the matching hook is the basename, camelCased. The path encodes the feature scope, so the name reflects that scope - not just the local concept. This keeps names unique across the app and makes a class findable from its file alone.

// file: account_onboarding/state/account_onboarding_champions_state.dart

// DON'T - drops the parent feature; collides with any other ChampionsState
class ChampionsState { /* ... */ }
AccountOnboardingChampionsState useChampionsState() { /* ... */ }

// DO - class = basename PascalCased, hook = basename camelCased
class AccountOnboardingChampionsState { /* ... */ }
AccountOnboardingChampionsState useAccountOnboardingChampionsState() { /* ... */ }

Private helper classes inside the file are exempt.

Global states name the domain, not the screen

A global state is shared app-wide, so it is named after the concern it owns - AuthState, SettingsState, CartState - never after a screen or a UI surface. Its hook follows the same use<Name>State rule.

// DON'T - a screen name on app-wide state, or a "Manager"/"Service" suffix
class LoginScreenAuthState { /* ... */ }
class AuthManager { /* ... */ } // <- that's a service, registered via useInjected

// DO - the domain it represents
class AuthState { /* ... */ }
AuthState useAuthState() { /* ... */ }

Reserve Service / Repository / Client suffixes for the injected dependencies a state hook reads via useInjected - they are a different layer from the State a hook produces.

Actions are named onX

Action fields on a State class - the callbacks the View invokes - are named for the user gesture that triggers them, prefixed on: onSavePressed, onItemTapped, onRefresh. Data fields are nouns. The prefix tells you at a glance which fields are behavior and which are values.

// DON'T - verbs that read like the hook's internals, or a plain noun for an action
class CartScreenState {
final void Function() checkout; // <- is this a value or a callback?
final void Function() doRemoveItem;
}

// DO - on<Gesture>; the View reads it as "what happens when the user does X"
class CartScreenState {
final void Function() onCheckoutPressed;
final void Function(String id) onRemovePressed;
}

Callbacks the Screen passes into the hook describe intent rather than a gesture - navigateToDetail, showErrorSnackbar, close. The hook turns those into the onX actions it exposes on the State.

Files are snake_case

Dart files use snake_case, and a screen's three parts sit in a predictable layout:

screens/checkout
|- checkout_screen.dart - Screen (coordinator)
|- state
| |- checkout_screen_state.dart - State class + hook
|- view
| |- checkout_screen_view.dart - View
// DON'T
CheckoutScreen.dart
checkoutScreenState.dart

// DO
checkout_screen.dart
checkout_screen_state.dart

See also