name: utopia-hooks description: > Flutter state management with utopia_hooks. Applies when writing Flutter screens, adding shared app state, handling async operations, building paginated / infinite-scroll lists, composing tab / bottom-nav / multi-page shells, sequencing app startup and bootstrap, handling global and retryable errors, building form fields (useFieldState, TextEditingControllerWrapper), unit-testing hook states (SimpleHookContext), injecting services, or migrating away from StatefulWidget. Covers the Screen/State/View pattern, the hook catalog, global state registration, async / paginated / submit hooks, navigation conventions, and dependency injection. license: BSD-2-Clause metadata: author: UtopiaSoftware tags: flutter, dart, utopia_hooks, state-management, hooks, pagination, infinite-scroll, navigation, bootstrap, error-handling, testing, forms
utopia_hooks - Flutter State Management
Overview
Holistic state management for Flutter using hooks. Every screen follows the
Screen → State → View tripartite pattern. Shared app state lives in
StateClass + hook + _providers. All logic belongs in hooks - never in widgets.
Crazy* widgets in examples (CrazyButton, CrazyTextField, …) are stand-ins for your
app's design system - substitute your own components or Material equivalents.
When to Apply
Reference these guidelines when:
- Building a new Flutter screen or adding a feature to an existing one
- Adding shared app-wide state (auth, settings, data caches, …)
- Handling async operations, form submissions, or loading states
- Building paginated / infinite-scroll lists, feeds, paginated search, or chat history
- Composing a tab / bottom-nav / multi-page shell screen
- Sequencing app startup (splash, SDK init, ordered providers) or wiring global error handling
- Declaring routes or navigating in reaction to state changes
- Injecting a service into a screen or registering a new dependency
- Unit-testing a hook state in isolation
- Reviewing Flutter code - looking for logic in View, widgets in State, or raw
setStatepatterns - Migrating from
StatefulWidget(for BLoC/Cubit migration, use the dedicatedutopia-hooks-migrate-blocplugin)
Priority-Ordered Guidelines
Full documentation with code examples in references/. Impact ratings: CRITICAL (always apply), HIGH (significant correctness/quality gain), MEDIUM (worthwhile improvement).
| Priority | File | Impact | Description |
|---|---|---|---|
| 1 | screen-state-view.md | CRITICAL | 3-file screen pattern: Screen, State class + hook, View; lightweight tier; dialogs with results |
| 2 | hooks-reference.md | CRITICAL | Full hook catalog: useState, useMemoized, useEffect, useProvided, useInjected, useIf, useMap, computed states, wrappers |
| 3 | global-state.md | CRITICAL | App-wide state: StateClass, HasInitialized, MutableValue, _providers registration, global-state idioms |
| 4 | async-patterns.md | HIGH | useSubmitState, useAutoComputedState, useMemoizedStream, retryable errors, lifecycle effects, sticky values |
| 5 | paginated.md | HIGH | usePaginatedComputedState + PaginatedComputedStateWrapper: cursor/page/token schemes, loadMore, refresh, debounce, dedup, optimistic overlay |
| 6 | app-bootstrap.md | HIGH | Ordered _providers, HasInitialized chains, combined initialization state, splash gating, SDK init races, retryable bootstrap |
| 7 | error-handling.md | HIGH | Where let-it-crash errors land: app-root catcher (zone + FlutterError.onError), error stream + root dialog, Retryable retry, typed errors to field errors |
| 8 | navigation.md | HIGH | Route declaration conventions, typed args, reactive navigation effects, status-driven redirects, screen-as-sheet/dialog with typed results, one-shot event fields |
| 9 | multi-page-shell.md | HIGH | Shell-with-N-pages composition: shell is Screen/State/View, each inner page is Screen/State/View; enum/index, IndexedStack/PageView/TabBarView, local/global index |
| 10 | complex-state-examples.md | HIGH | Five anonymised reference shapes for complex state (pipeline / dashboard / parent-owned list / per-item widget-level / multi-step flow) |
| 11 | composable-hooks.md | HIGH | Widget-level hooks, composed hook state, screen hook decomposition, per-item state archetypes |
| 12 | flutter-conventions.md | HIGH | IList/IMap/ISet, it lambdas, strict analyzer, widget extraction, spacing, generated code, TextEditingController |
| 13 | testing.md | HIGH | Unit testing hooks with SimpleHookContext and SimpleHookProviderContainer - no widget tree needed; mocking injected services |
| 14 | di-services.md | MEDIUM | utopia_injector, useInjected, DI bridge hooks, service types, get_it fallback |
| 15 | utopia-cli.md | MEDIUM | utopia_cli agent surfaces: project inspection (describe), screen scaffolding (add screen --json), repo audit (doctor), hooks analyze variants, MCP server setup |
Quick Reference - Top Patterns
A pointer-paragraph for the four most-common entry points. For everything else, jump straight to the guidelines table above. Do not extrapolate from the summaries.
Screen architecture → screen-state-view.md
Every screen = 3 files: feature_screen.dart (HookWidget, pure wiring - builds nav callbacks from BuildContext, calls exactly one useXScreenState(...)), state/feature_screen_state.dart (immutable State class + hook with all logic), view/feature_screen_view.dart (StatelessWidget, View receives only state).
Global state registration → global-state.md
State class (often extends HasInitialized) + useXState() hook + entry in _providers map at app root. Consume with useProvided<XState>() inside any state hook. ValueProvider for static/already-computed values.
Async - download / upload / stream → async-patterns.md
- Download (read, one-shot) →
useAutoComputedState- auto-fetches, re-runs onkeyschange,shouldComputegates prerequisites - Upload (write) →
useSubmitState- user-triggered; tracks in-flight runs but does NOT block duplicate calls (the three guards are in async-patterns.md); let errors crash by default - Stream (reactive) →
useMemoizedStream- subscribes continuously, re-subscribes onkeyschange
Paginated lists → paginated.md
Any cursor-based list - feed, search results, chat history - uses usePaginatedComputedState + PaginatedComputedStateWrapper (scroll listener + pull-to-refresh). Never hand-roll useState<List<T>> + hasMore + cursor. Cursor is opaque (int for offset/page, String? for token). Optimistic mutations go in a local override layer, not into items.
Searching References
All paths below are relative to the skill root (the directory containing this SKILL.md).
From elsewhere, use the absolute form, e.g.
grep -rl "useSubmitState" /path/to/plugins/utopia-hooks/skills/utopia-hooks/references/.
# Async / data-loading hooks
grep -rl "useAutoComputedState" references/
grep -rl "useComputedState" references/
grep -rl "useSubmitState" references/
grep -rl "useMemoizedStream" references/
grep -rl "usePaginatedComputedState\|PaginatedComputedStateWrapper" references/
grep -rl "useDebounced" references/
# Core / lifecycle hooks
grep -rl "useMemoized" references/
grep -rl "useEffect" references/
grep -rl "useMap" references/
grep -rl "useIf" references/
grep -rl "useKeyed" references/
# Form / input hooks
grep -rl "useFieldState" references/
grep -rl "useFocusNode" references/
# Global state + DI types
grep -rl "HasInitialized" references/
grep -rl "MutableValue" references/
grep -rl "useProvided" references/
grep -rl "useInjected" references/
# Bootstrap / errors / navigation
grep -rl "Retryable" references/
grep -rl "HasInitialized.all" references/
grep -rl "useCombinedInitializationState" references/
grep -rl "buildRoute" references/
Validation - utopia_cli Quality Gate
The canonical utopia_hooks convention analyzer lives in utopia_cli, not in this skill's
shell scripts - Claude, Codex, CI, and pre-commit all run the same rules. The plugin's
PostToolUse hook validates every Dart edit via utopia hooks analyze --hook-json; for
manual / batch / changed-file / CI variants, repo-wide utopia doctor audits, project
inspection (utopia describe), scaffolding (utopia add screen --json), and the
utopia mcp server, see utopia-cli.md.
Dart Tooling - Prefer Dart MCP
When analyzing, testing, formatting, or fixing Dart code, prefer Dart MCP tools over their bash equivalents - they return structured results and pick up the active SDK (including fvm-pinned versions) automatically.
| Task | Dart MCP (preferred) | Bash fallback |
|---|---|---|
| Static analysis | analyze_files |
dart analyze / flutter analyze |
| Run tests | run_tests |
dart test / flutter test |
| Format code | dart_format |
dart format |
| Apply dart fixes | dart_fix |
dart fix --apply |
| Pub operations | pub |
dart pub get / flutter pub add |
Use bash only in shell-only contexts (CI, pre-commit hooks) or when no MCP equivalent
exists (build_runner, melos, flutter build, ffigen). Setup: if claude mcp list
doesn't show dart, run claude mcp add -s user dart -- fvm dart mcp-server
(drop fvm if the repo doesn't use it).
Problem → Skill Mapping
| Problem | Start With |
|---|---|
| Adding a new screen | utopia-cli.md (utopia add screen --json) → screen-state-view.md |
| What screens/routes/global states already exist in this project? | utopia-cli.md (utopia describe) |
| Repo-wide convention audit / CI gate | utopia-cli.md (utopia doctor) |
| Logic is leaking into the View | screen-state-view.md |
| Widget imports in a State class | screen-state-view.md |
| App-wide state (auth, config, data) | global-state.md |
| Screen not reacting to state changes | global-state.md → hooks-reference.md |
| App startup / splash / boot sequencing | app-bootstrap.md |
| Global states that depend on other global states | app-bootstrap.md |
| Firebase/SDK not initialized race | app-bootstrap.md |
| Where do uncaught submit/compute errors go? | error-handling.md |
| Retry dialog for failed operations | error-handling.md |
| Map backend errors to form field messages | error-handling.md |
| Declaring routes / typed navigation | navigation.md |
| Navigate in reaction to global state (post-login redirect) | navigation.md |
| Screen hosted in bottom sheet / dialog returning a result | navigation.md |
| Form submission with loading/error | async-patterns.md |
| Form with validation (multi-field, submit gating) | async-patterns.md |
| Async data with loading spinner | async-patterns.md |
| Paginated list, infinite scroll, cursor/page/token pagination | paginated.md |
| Pull-to-refresh on a list | paginated.md (PaginatedComputedStateWrapper) |
| Paginated search with debouncing | paginated.md (keys + debounceDuration) |
| Optimistic updates on a paginated list | paginated.md (optimistic overlay) or complex-state-examples.md shape 3 |
| Stream that should drive UI | hooks-reference.md (useMemoizedStream) |
| Derived value from other state | hooks-reference.md (useMemoized) |
| Widget with expand/collapse, animation, lazy load | composable-hooks.md (widget-level hook) |
| Reusable widget used N times on one screen | composable-hooks.md (composed hook state) |
| Screen state polluted with per-tile logic | composable-hooks.md (widget-level hook) |
| Paging, specialized text field, reusable control | composable-hooks.md (composed hook state) |
| TextEditingController / FocusNode handling | flutter-conventions.md |
| Testing a screen state hook | testing.md |
| Testing global state and state interactions | testing.md |
| Injecting a service into a screen | di-services.md |
| Registering a new service or state | di-services.md |
Using List / Map / Set instead of immutable |
flutter-conventions.md |
| Lambda style, naming, widget extraction | flutter-conventions.md |
| Generated code out of date | flutter-conventions.md |
| Replacing StatefulWidget | screen-state-view.md + hooks-reference.md |
| State hook is too large (>300 lines, >10 useState) | composable-hooks.md (screen hook decomposition, Pattern 3) |
| Per-item state (list tile with expand / async / drafts) | composable-hooks.md (per-item state archetypes) |
| Complex multi-domain or multi-step screen state - what should it look like? | complex-state-examples.md |
| Screen with bottom nav / tabs / sub-pages the user switches between | multi-page-shell.md |
| Inner tab/page is a monolithic HookWidget with inline logic | multi-page-shell.md |
| Bottom nav / tab index needs to survive deep links or cross-screen jumps | multi-page-shell.md + global-state.md |
Non-Negotiable Rules
- View never calls hooks - no
useState,useProvided,useInjectedin*_view.dart. View is alwaysStatelessWidget. - View constructor takes ONLY
state- no extraonBack,onNavigate, or other parameters. All callbacks are fields on the State class. - Screen = pure wiring - Screen's
build()readsBuildContext(for navigation/dialogs/args) and calls exactly one hook:useXScreenState(...). Screen must NOT calluseInjected,useProvided,useEffect,useState, or any other hook (single exception: oneuseEffectwhose only job is consuming one-shot event fields - see navigation.md). - Navigation flows Screen → State → View as callbacks - never
useProvided<NavigatorKey>oruseInjected<AppRouter>. State hook receives navigation asvoid Function()/Future<T?> Function()parameters. See navigation.md. - State never imports widgets - no Flutter widget imports in
*_screen_state.dart useProvided/useInjectedonly in screen state hooks - not in Screen, not in View, not passed down as parameters- No mutable collections in State classes - always
IList/IMap/ISet, neverList/Map/Set, including static data - No manual loading state - never use
useState<bool>+try/catch/finallyfor data loading. AlwaysuseAutoComputedState. - No hand-rolled pagination - never use
useState<List<T>>+hasMore+cursorfor paginated lists. AlwaysusePaginatedComputedState+PaginatedComputedStateWrapper. See paginated.md. - Never construct
ButtonStateby hand for submit-backed buttons - alwayssubmitState.toButtonState(onTap: ...)oruseSubmitButtonState(only the latter guards re-entrant taps - see async-patterns.md). - Prefer
useMemoizedoveruseEffectfor derived state - effects cascade; memoized values don't - One State class per screen - all screen data in one place, not scattered
useStatecalls across the widget tree - Never wrap
TextEditingControllerinuseMemoized+useListenable- alwaysuseFieldStatein the state hook +TextEditingControllerWrapperin the View. See flutter-conventions.md. - View files ≤ ~300 lines - extract complex widgets to
widget/folder, using widget-level hook pattern from composable-hooks.md when they have own state - Screen files ≤ ~100 lines (soft redflag) - Screen is pure wiring; if over ~100 it almost always holds Scaffold/layout chrome (belongs in View) or business logic (belongs in state hook). Typical Screen is 30-80 lines. See screen-state-view.md.
- Global state hooks ≤ ~300 lines - same threshold as screen state hooks. If over, the global is spanning multiple domains; split into separate globals (one per domain in
_providers), or extract pure helpers/derivations to services. See global-state.md.
The one sanctioned exception - the lightweight tier. A trivial, self-contained dialog
or component (roughly 30 lines, no navigation fan-out) may be a single-file HookWidget
calling useInjected / useSubmitState directly in build. The moment it grows - second
action, derived state, navigation, anything that smells like logic - promote it to the
full Screen/State/View split. Three files stays the default for every screen.
See screen-state-view.md (lightweight tier).
Self-Audit Checklist
After generating a screen, run down this list. Each item is a one-line check for a rule above (or a reference pattern) - see the rule for the full rationale and fix:
- View constructor takes anything beyond
state? → "View constructor takes ONLYstate" - Screen calls any hook other than
useXScreenState(...)? → "Screen = pure wiring" (exception: oneuseEffectconsuming one-shot event fields - see navigation.md) - View extends
HookWidgetor calls hooks? → "View never calls hooks" useState<bool>(true)/useState<T?>(null)for loading/error? → "No manual loading state"- Mutable
List/Map/Setin the State class? → "No mutable collections in State classes" useState<List<T>>+hasMore+cursor+ manual load effect? → "No hand-rolled pagination"- Hand-built
ButtonState(...)next to a submit state? → "Never constructButtonStateby hand" useProvided<NavigatorKey>/useInjected<AppRouter>in a state hook? → "Navigation flows Screen → State → View as callbacks"useXScreenState(...)signature acceptsBuildContext? → same rule; replace with typed callbacks built in the ScreenuseMemoized(TextEditingController.new)+useListenable? → "Never wrapTextEditingController"- Derived value via
useEffect+useState? → "PreferuseMemoizedoveruseEffect" - Two or more
*ScreenStateclasses for one screen, or scattereduseStatein the View? → "One State class per screen" - Widget imports in
*_screen_state.dartbeyondColor/Duration/ domain types? → "State never imports widgets" - View file > ~300 lines? → "View files ≤ ~300 lines"
- Screen file > ~100 lines? → "Screen files ≤ ~100 lines (soft redflag)"
- State hook > ~300 lines or > ~10
useState? → decompose; composable-hooks.md Pattern 3 - Global state hook > ~300 lines? → "Global state hooks ≤ ~300 lines"
- More than 2
useSubmitState()in one hook? → group mutually exclusive actions; async-patterns.md widgets/*.dartextendingHookWidgetand callinguseProvided/useInjected? → mis-classified View; rename toview/x_screen_view.dart, see screen-state-view.md Common Pitfalls
Attribution
Built on utopia_hooks by UtopiaSoftware.