Skip to content

File kinds ​

At a glance ​

text
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 routes

Each 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.

FileExportsIts constructor / signature can ask for
page.darta widgetsegments; query; what data.dart yields; the navigation extra
data.dartdata(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 sectionsegments, query (named)
action.dartaction(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'ssegments, query (named), and the one input
loading.darta widget, inherited by subfolderssegments; query
error.darta widget, inherited by subfolderssegments; query; error, stackTrace, retry; also a failed guard's view (since 0.21.0) when it takes only error, stackTrace, retry
layout.darta widget; wraps this folder and below (ShellRoute), or holds its subfolders as tabs. A tab layout can also export a container functionchild or navigationShell; segments at or above it; query; the section data it wraps or is inside; the navigation extra
guard.dartGuardResult 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.dartString redirect({…}) in place of page.dart: a route that only redirects; may take Ref ref first (since 0.5.0), or ProviderContainer curi; segments; query (named); extra
observe.dartvoid 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 lifecycleRef ref first; segments at or above its folder; query (named); uri; TypedLocation route; RouteScope scope (onEnter, since 0.11.0)
leave.dartLeaveResult 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 pageBuildContext context, then Ref ref (both optional); segments at or above its folder; query (named); uri; extra; PageLeave page
transition.dartPage<…> transition(…); applies to this folder and below, layouts' shells includedkey, child, state, shell (a bool)
present.dartPage<…> present(…): the app builds this route's own page (a sheet, say), on the root navigator; this folder onlykey, child, state
navigator.dartconst navigator = RouteNavigator.root;: this folder and below render on the root navigatornothing: it is data
not_found.darta widget, optional, in any folder (nearest wins; without one at the root, a plain "Nothing at /path" view); unknown paths and unparsable segmentsuri
meta.dartconst meta = <any const expression>;, beside a page.dart or redirect.dart: that route's own facts, passed untouched into the manifestnothing: it is data
extra_codec.dartat the root of the app folder only: a top-level extraCodec, the Codec<Object?, Object?> the router saves an extra withnothing: it is data
app.dartat 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.approuter (a GoRouter); other parameters must be optional
startup.dartat 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 onenothing: startup() takes no parameters
splash.dartat the root of the app folder only (since 0.8.1): a widget shown while an async startup() runs, and when it failserror, stackTrace, retry (each nullable: null while startup() runs)
route.dartconstants, in any folder: caseSensitive, paths, nest, linkable, remount, deferred, freshness (see below). Read from the source, never importednothing: it is data
nav.dartin 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 headingnothing: 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:

dart
// lib/app/(kyc)/shop/name/page.dart
import 'package:my_app/screens/kyc/legal_name_screen.dart';

Widget page() => const LegalNameScreen(audience: KycAudience.shop);
dart
// 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() and notFound() (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 no BuildContext and no WidgetRef (asking for one is an error that says so). Hooks and ref belong in the widget it returns.

  • The route class name. A class names its route after itself (ProductPage → ProductRoute). A page() takes the folder path instead, ignoring (group) folders and joining the segments in PascalCase: (kyc)/shop/name/page.dart is ShopNameRoute, orders/$orderId/cancel/page.dart is OrdersOrderIdCancelRoute (the root is RootRoute). To pick another, put a string literal in page.dart:

    dart
    const 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, and meta.dart works beside a function page. Two routes with the same name are an error that suggests routeName.

  • export isn't followed. page.dart has 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:

  1. By name. A parameter named like a $segment in the path gets that segment. data, child, navigationShell (or shell), error, stackTrace, retry, uri and extra (in a page, a layout, a guard or a redirect) get what their name says, in the files where they make sense.
  2. Query. An optional parameter that is nullable or a List of String/int/double/bool (or of an enum, or of a typedef of one of those since 0.21.0) is a query parameter: int? page gets ?page=2, and List<String> tags = const [] gets every ?tags=.
  3. By type. Otherwise, a page's parameter whose type is what data.dart yields gets the data, so required this.product with final Product product; works. (A page or layout below a section can take the section's data the same way.) An error view's Object gets the error, StackTrace the stack trace and VoidCallback the retry. A layout's Widget gets the child, its StatefulNavigationShell gets the tab shell, and not-found's Uri gets the URI. Since 0.21.0 a type is what it names: typedef ProductView = Product; in lib/models/ids.dart is a Product, so required ProductView product gets the data that yields Product, and m.Product (with an import prefix) is the same type as Product. The spelling is tried first: a parameter typed with a typedef of a query type (rule 2) is the data, not a query parameter, when data.dart yields that type spelled the same. The app's own files are followed, and since 0.22.0 those of its path: dependencies; a typedef of a hosted, git: or SDK package is still compared as spelled (Known limitations).
  4. 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).

Released under the MIT License.