File kinds
At a glance
lib/main.dart Future<void> main() => AppMain.run(); (the whole main(), since 0.8.1)
lib/app/
app.dart App({required GoRouter router}) the MaterialApp.router around the router (root only, optional)
startup.dart Future<List<Override>> startup() before the app: overrides, observers, a zone (root only, optional)
splash.dart Splash({Object? error, VoidCallback? retry}) while startup() runs, and when it fails (root only, optional)
layout.dart AppLayout({required Widget child}) → ShellRoute
page.dart HomePage() → /
loading.dart RootLoading() (inherited)
error.dart RootError({required Object error, required VoidCallback retry})
not_found.dart NotFoundPage({required Uri uri}) (optional, in any folder)
transition.dart Page<void> transition(LocalKey key, Widget child) (inherited)
route.dart const caseSensitive = false; (inherited: this folder and below match in any case)
products/
route.dart const paths = {'fr': 'produits', 'de': 'produkte'}; (this folder also answers /produits, /produkte)
data.dart final data = FutureProvider<List<Product>>(…)
page.dart ProductsPage({required List<Product> products}) → /products
loading.dart ProductsLoading()
$id/
data.dart Future<Product> data(Ref ref, {required int id})
page.dart ProductPage({required Product product}) → /products/:id
action.dart Future<void> action(Ref ref, {required int id, required Refund input}) (a write)
error.dart ProductError({required int id, required Object error, …})
meta.dart const meta = PageMeta(code: 'B04', …) (this route's facts, any const)
checkout/
guard.dart GuardResult guard(Ref ref) (guards this and below)
page.dart
orders/$id/edit/
leave.dart LeaveResult leave(BuildContext context, Ref ref, {…, required PageLeave page}) (asked before this page goes)
old-products/$id/
redirect.dart String redirect({required int id}) → /old-products/:id redirects
greet/$name/page.dart GreetPage({required String name})
docs/$$rest/page.dart DocsPage({required List<String> rest}) → /docs/a, /docs/a/b, …
compare/$$ids/page.dart ComparePage({required List<int> ids}) → /compare/3/7 (each part an int)
(account)/ a group: its layout wraps profile/ and settings/,
layout.dart but adds nothing to their URLs (/profile, /settings)
profile/page.dart
settings/page.dart
teams/$teamId/ no page: its layout and data.dart cover the section below
data.dart Future<Team> data(Ref ref, {required String teamId})
layout.dart TeamLayout({required Team team, required Widget child})
members/page.dart MembersPage(this.team) → /teams/:teamId/members
not_found.dart TeamNotFound({required Uri uri}) (for unknown paths below)
_components/ private: never routesEach view file exports one widget class, of any kind (StatelessWidget, ConsumerWidget, HookConsumerWidget and so on), or a top-level function that returns a widget; function files export one top-level function. Other public classes may sit in the file as long as exactly one of them extends a …Widget class (a class Helper {} beside a StatelessWidget is fine). When fsp can't tell which is the view it says "expected one public widget class" and lists them: make helpers private (_Name) rather than lean on that.
| File | Exports | Its constructor / signature can ask for |
|---|---|---|
page.dart | a widget | segments; query; what data.dart yields; the navigation extra |
data.dart | data(Ref ref, {…}) returning Future<T>, Stream<T> or T — or ProviderListenable<AsyncValue<T>> data({…}) selecting a provider you have — or final data = <Provider>(…). Beside a page.dart it feeds the page; in a page-less folder with a layout.dart, the whole section | segments, query (named) |
action.dart | action(Ref ref, {…, required Input input}) (any number of functions of that shape) returning Future<T>, FutureOr<T> or T, and optionally const invalidates = [...]. Beside a page.dart it is that route's write; in a page-less folder with a layout.dart, the section's | segments, query (named), and the one input |
loading.dart | a widget, inherited by subfolders | segments; query |
error.dart | a widget, inherited by subfolders | segments; query; error, stackTrace, retry; also a failed guard's view (since 0.21.0) when it takes only error, stackTrace, retry |
layout.dart | a widget; wraps this folder and below (ShellRoute), or holds its subfolders as tabs. A tab layout can also export a container function | child or navigationShell; segments at or above it; query; the section data it wraps or is inside; the navigation extra |
guard.dart | GuardResult guard(Ref ref, {…}); GuardResult is FutureOr<String?>: a location to redirect to, or null to let the navigation through. Guards every route at and below its folder, and runs again when what it ref.watches changes (since 0.5.0; ProviderContainer c first is the older form, read once) | uri; segments at or above its folder; query (named); extra |
redirect.dart | String redirect({…}) in place of page.dart: a route that only redirects; may take Ref ref first (since 0.5.0), or ProviderContainer c | uri; segments; query (named); extra |
observe.dart | void onEnter(Ref ref, {…}), onLeave and onFocus (any of them): run for every page at and below its folder when it is entered, left and focused (since 0.8.1). See Route lifecycle | Ref ref first; segments at or above its folder; query (named); uri; TypedLocation route; RouteScope scope (onEnter, since 0.11.0) |
leave.dart | LeaveResult leave(BuildContext context, Ref ref, {…}) (LeaveResult is FutureOr<bool>): asked before this folder's page goes, true to let it go, false to keep it (since 0.11.0). Not inherited; it needs a page.dart, except beside the layout.dart of a flow section (since 0.11.0), where it is each step's. See Leaving a page | BuildContext context, then Ref ref (both optional); segments at or above its folder; query (named); uri; extra; PageLeave page |
transition.dart | Page<…> transition(…); applies to this folder and below, layouts' shells included | key, child, state, shell (a bool) |
present.dart | Page<…> present(…): the app builds this route's own page (a sheet, say), on the root navigator; this folder only | key, child, state |
navigator.dart | const navigator = RouteNavigator.root;: this folder and below render on the root navigator | nothing: it is data |
not_found.dart | a widget, optional, in any folder (nearest wins; without one at the root, a plain "Nothing at /path" view); unknown paths and unparsable segments | uri |
meta.dart | const meta = <any const expression>;, beside a page.dart or redirect.dart: that route's own facts, passed untouched into the manifest | nothing: it is data |
extra_codec.dart | at the root of the app folder only: a top-level extraCodec, the Codec<Object?, Object?> the router saves an extra with | nothing: it is data |
app.dart | at the root of the app folder only (since 0.8.1): a widget (any kind) that gets the router and builds the MaterialApp.router around it; optionally GoRouter router() too. AppMain.app | router (a GoRouter); other parameters must be optional |
startup.dart | at the root of the app folder only (since 0.8.1): startup() (before the app; may return the Overrides), zone(), providerObservers, routerObservers, retry(); at least one | nothing: startup() takes no parameters |
splash.dart | at the root of the app folder only (since 0.8.1): a widget shown while an async startup() runs, and when it fails | error, stackTrace, retry (each nullable: null while startup() runs) |
route.dart | constants, in any folder: caseSensitive, paths, nest, linkable, remount, deferred, freshness (see below). Read from the source, never imported | nothing: it is data |
nav.dart | in any folder (since 0.8.1): const nav = Nav(label: 'Products', order: 1); — how the folder shows in the generated menus and breadcrumbs (AppMenu) — and optionally String label(BuildContext context, {…}), the label shown, localized. Read from the source (its order and the segments label() asks for); a folder with no page is a heading | nothing: it is data; label() takes a BuildContext and the segments of its folder and above (named, required) |
What route.dart can hold. Each constant is optional: caseSensitive, paths, nest, linkable, remount, deferred and freshness. Configuration lists each one with its rules.
Since 0.8.1 an action.dart may also hold the companions of an action: its validate(), its optimistic() and, with the fespalier_forms package (since 0.11.0), its form(). They are functions in that file, not a file kind.
Function views
page.dart, loading.dart, error.dart, layout.dart and not_found.dart can export a top-level function named after the file, returning a Widget, instead of a widget class:
// lib/app/(kyc)/shop/name/page.dart
import 'package:my_app/screens/kyc/legal_name_screen.dart';
Widget page() => const LegalNameScreen(audience: KycAudience.shop);// lib/app/(buyer)/orders/$orderId/cancel/page.dart
Widget page({required String orderId, String? back}) =>
CancelOrderScreen(orderId: orderId, back: back);That is one route per file, however many routes build the same screen, and the screen can stay where it is (lib/screens/…) instead of moving into lib/app/. The function's parameters are filled exactly like a constructor's (below): segments and query parameters by name, data by name or by type, child or a shell for a layout(), error, stackTrace and retry for an error(), uri for a notFound(). Named and positional parameters both work, and a binding error points at the parameter.
Names.
page(),loading(),error(),layout()andnotFound()(not_found()too). Other functions in the file are helpers and are ignored.One form per file. A file with a public widget class and the function is an error that names both. To use the function, keep the widget in another file (or make it private) and build it from the function.
No hooks, no
ref. A function view is a plain function: it has noBuildContextand noWidgetRef(asking for one is an error that says so). Hooks andrefbelong in the widget it returns.The route class name. A class names its route after itself (
ProductPage→ProductRoute). Apage()takes the folder path instead, ignoring(group)folders and joining the segments in PascalCase:(kyc)/shop/name/page.dartisShopNameRoute,orders/$orderId/cancel/page.dartisOrdersOrderIdCancelRoute(the root isRootRoute). To pick another, put a string literal inpage.dart:dartconst routeName = 'KycShopName'; // KycShopNameRoute Widget page() => const LegalNameScreen(audience: KycAudience.shop);It must be an UpperCamelCase name (the route class is
<routeName>Route), and it also renames a class-form page's route. The route manifest lists the route under this name, andmeta.dartworks beside a function page. Two routes with the same name are an error that suggestsrouteName.exportisn't followed.page.darthas to hold the function itself, so it stays the source of truth for the route.
fsp new 'shop/name' --function scaffolds the function form (--name KycShopName writes the routeName).
File names
The one file kind with two words is not_found.dart. It is also read in kebab-case, not-found.dart, whatever the configuration says, so a project that names every Dart file in kebab-case can keep to that, and a tree that mixes the two still works. Both in one folder is an error with a code frame for each file. Diagnostics, fsp routes and the header of app.g.dart name a file as it is spelled on disk.
file_style: snake | kebab (default snake) only picks what fsp init and fsp new write. The single-word kinds (page.dart, layout.dart, …) have one spelling.
How parameters are filled
The generator reads each constructor (named or positional, this.x or typed) and fills every parameter, trying these rules in order:
- By name. A parameter named like a
$segmentin the path gets that segment.data,child,navigationShell(orshell),error,stackTrace,retry,uriandextra(in a page, a layout, a guard or a redirect) get what their name says, in the files where they make sense. - Query. An optional parameter that is nullable or a
ListofString/int/double/bool(or of an enum, or of atypedefof one of those since 0.21.0) is a query parameter:int? pagegets?page=2, andList<String> tags = const []gets every?tags=. - By type. Otherwise, a page's parameter whose type is what
data.dartyields gets the data, sorequired this.productwithfinal Product product;works. (A page or layout below a section can take the section's data the same way.) An error view'sObjectgets the error,StackTracethe stack trace andVoidCallbackthe retry. A layout'sWidgetgets the child, itsStatefulNavigationShellgets the tab shell, and not-found'sUrigets the URI. Since 0.21.0 a type is what it names:typedef ProductView = Product;inlib/models/ids.dartis aProduct, sorequired ProductView productgets the data that yieldsProduct, andm.Product(with an import prefix) is the same type asProduct. The spelling is tried first: a parameter typed with atypedefof a query type (rule 2) is the data, not a query parameter, whendata.dartyields that type spelled the same. The app's own files are followed, and since 0.22.0 those of itspath:dependencies; a typedef of a hosted,git:or SDK package is still compared as spelled (Known limitations). - Otherwise, a required parameter is a generator error pointing at it. An optional one is left to its default.
These names are reserved, so segments can't use them. Parameters bound by name are type-checked: uri must be a Uri, child a Widget, error an Object, stackTrace a StackTrace, retry a VoidCallback, navigationShell a StatefulNavigationShell, and a transition's key and state a LocalKey and a GoRouterState. Declaring one as anything else is an error at that parameter (Object and dynamic always fit).