Search the site
APP
Add a page by adding a file. Its URL comes from where the file lives.
Link to it through a typed DVRoutes target, so a moved page is a compile error.
ON THIS PAGE
Add a page with a file
Or declare routes in a file, if you prefer
Write logic in a page body
Read route and query parameters
Navigate with typed DVRoutes targets
Link with DVNavLink
Share chrome with _layout.dart
Guard a folder with _guard.dart
Show loading and error states
Handle unknown URLs
Set the title and sitemap entry
Layouts, tabs and somebody else's router
Status
Functional
Class
// lib/pages/about.dart is served at /about.
import 'package:flutter/widgets.dart';
import '../dartvel_client/dartvel_client.dart';
@DVPage(title: 'About us')
Widget _aboutPage(BuildContext context) => const DVBox.list(<Widget>[
DVText('About us'),
DVText('We make tools for Flutter teams.'),
]);Copy code to clipboard
lib/pages/index.dart
URL: /
lib/pages/about.dart
URL: /about
lib/pages/blog/index.dart
URL: /blog
lib/pages/blog/[id].dart
URL: /blog/:id
lib/pages/(marketing)/pricing.dart
URL: /pricing
A folder in parentheses groups files without adding to the URL.
A file is a page when it has @DVPage or ends in .page.dart.
Two files that resolve to one URL stop the build.
A file under lib/pages is the short way. A route can also be declared in code, in lib/routes.dart, which suits a screen that is not a page of its own, a path built from something else, or a codebase that already keeps its routes in one place.
// lib/routes.dart, read by dartvel routes. Each route here gets a typed
// target on DVRoutes beside the pages' own, and the generated router mounts
// the list with them: one router, one DVRoutes, whichever way a route was
// declared.
final List<DVRouteNode> routes = <DVRouteNode>[
DVRoute(
path: '/settings',
title: 'Settings',
builder: (BuildContext context, DVRouteState state) =>
SettingsScreen(tab: state.query['tab']),
),
DVRoute(
path: '/roasters',
title: 'The roasters',
builder: (BuildContext context, DVRouteState state) => const RoastersScreen(),
routes: <DVRouteNode>[
DVRoute(
// Nested, so this is /roasters/:person. The name is given because
// /roasters/:person would derive DVRoutes.roasters, which the parent
// already is.
path: ':person',
name: 'roaster',
builder: (BuildContext context, DVRouteState state) =>
RoasterScreen(person: state.params['person']!),
),
],
),
];Copy code to clipboard
dartvel routes reads the file, so each route gets a typed DVRoutes target beside the pages' own. One router, one DVRoutes, whichever way a route was declared.
DVRoute nests through routes:. A nested path joins its parent's, so :person under /roasters is /roasters/:person.
DVShellRoute and DVStatefulShellRoute wrap children in shared chrome, and take a redirect that may be async, so a session check shows the pending view and never a blank screen.
DVRoute is a screen, DVShellRoute wraps its children in a frame, and DVStatefulShellRoute keeps a stack per branch. DVGoRoutes mounts a GoRoute list you already have.
dartvel.routes in pubspec.yaml points at another file. A route the reader cannot parse stops the build with DV-ROUTE-003. It is not skipped: a skipped route still runs, with no typed target and no page on the web.
A page function can have a block body, with signals and local variables before it returns its widgets.
Functional
Class
// lib/pages/team.dart is served at /team.
@DVPage(title: 'Team')
Widget _teamPage(BuildContext context) {
final DVSignal<bool> showAll = context.signal(false);
final List<String> people = <String>['Ada', 'Grace', 'Linus', 'Margaret'];
final List<String> shown = showAll.value ? people : people.take(2).toList();
return DVBox.list(<Widget>[
for (final String name in shown) DVText(name),
DVText(showAll.value ? 'Show fewer' : 'Show everyone').modifier(
DVModifier().semanticButton().onTap(() => showAll.value = !showAll.value),
),
]);
}Copy code to clipboard
The function stays private. Generation writes the public page and its route.
Helpers declared in the page's own file still resolve after generation moves the body.
Name a file or folder [id] and read the value from context.dvParams. Query strings are in context.dvQuery.
Functional
Class
// lib/pages/blog/[id].dart is served at /blog/:id.
import 'package:flutter/widgets.dart';
import '../../dartvel_client/dartvel_client.dart';
@DVPage(title: 'Blog post')
Widget _blogPostPage(BuildContext context) => DVBox.list(<Widget>[
DVText('Post ${context.dvParams['id']}'),
DVText('Sorted by ${context.dvQuery['sort'] ?? 'date'}'),
]);Copy code to clipboard
Generation writes a DVRoutes class with one target per page. A page with parameters is a function.
DVRoutes.index // /
DVRoutes.about // /about
DVRoutes.blog(id: '42') // /blog/42Copy code to clipboard
DVText('Open post 7').modifier(
DVModifier().onTap(() => context.navigateToPage(DVRoutes.blog(id: '7'))),
),
DVText('Back to home').modifier(
DVModifier().onTap(DV.Navigation.to(DVRoutes.index)),
),
// A query is still a typed route. Both halves are generated targets,
// so a page that moves is a compile error here.
DVText('Sign in and come back').modifier(
DVModifier().onTap(DV.Navigation.to(
DVRoutes.accountsecurity.withQuery(<String, String>{
'from': DVRoutes.accountsettings.path,
}),
)),
),Copy code to clipboard
context.navigateToPage(target) goes there now.
DV.Navigation.to(target) returns a callback for onTap.
target.withQuery(...) adds a query and stays a route. /sign-in?from=/account is two generated targets in one call, so either page moving is a compile error. Values are encoded.
DV.Navigation also has push, back, canGoBack and currentPath.
DVNavLink(
to: DVRoutes.about,
child: const DVText('About us'),
),Copy code to clipboard
Tab, Enter and middle-click behave like a web link.
The target page starts loading after 300 ms on screen.
A pointer resting on the link for 900 ms, or a long press on a phone, shows a live preview of the page.
DVNavLink(
to: DVRoutes.blog(id: '42'),
preload: DVLinkPreload.hover, // none, hover, visible, immediate
preview: DVLinkPreview.none, // none, auto
child: const DVText('Read post 42'),
),
DVNavLink.external(
'https://pub.dev/packages/dartvel_dev',
child: const DVText('Dartvel on pub.dev'),
),Copy code to clipboard
DVLinkPreload: none, hover, visible (the default) or immediate.
DVLinkPreview: auto (the default), none, or widget(child).
Every route has a preview. A link to /orders/9 previews /orders/:id with id = 9.
A page behind a guard previews only its title and that it needs signing in. The page itself is never built on a hover, so nothing the guard protects is shown.
Tap or scroll outside a preview to close it. Back closes the preview before leaving the page.
DVNavLink.external opens another site and never preloads.
DVNavLink(
to: DVRoutes.about,
preview: DVLinkPreview.widget(
const Padding(
padding: .all(16),
child: DVText('Meet the people building our app.'),
),
),
child: const DVText('About us'),
),Copy code to clipboard
A custom preview replaces the destination page with your widget, including its buttons and scrolling. It gets at most 340 × 240 logical pixels, reduced to fit the safe screen area and keyboard. Use normal constrained layouts inside it. It does not need a registered destination preview.
Keep GlobalKeys inside the page
A preview builds a second live copy of the target page. Keys shared by the whole program cannot be in two places, so create them in the page's own State.
A _layout.dart wraps every page in its folder and below. It extends DartvelLayout and places child.
// lib/pages/_layout.dart wraps every page in lib/pages.
import 'package:flutter/widgets.dart';
import '../dartvel_client/dartvel_client.dart';
class const Layout({super.key, required super.child}) extends DartvelLayout {
@override
Widget build(BuildContext context) => DVBox.list(<Widget>[
DVBox.row(<Widget>[
DVNavLink(to: DVRoutes.index, child: const DVText('Home')),
DVNavLink(to: DVRoutes.about, child: const DVText('About')),
], spacing: 16),
Expanded(child: child),
]);
}Copy code to clipboard
// lib/pages/blog/_layout.dart wraps the pages under /blog,
// inside the root layout.
import 'package:flutter/widgets.dart';
import '../../dartvel_client/dartvel_client.dart';
class const BlogLayout({super.key, required super.child})
extends DartvelLayout {
@override
Widget build(BuildContext context) => DVBox.row(<Widget>[
const DVText('Blog'),
Expanded(child: child),
], spacing: 24);
}Copy code to clipboard
A page under /blog is wrapped by the root layout first, then by the blog layout.
// lib/pages/account/_guard.dart runs before every page under /account.
import 'dart:async';
import 'package:flutter/widgets.dart';
import '../../dartvel_client/dartvel_client.dart';
FutureOr<String?> guard(BuildContext context, GoRouterState state) {
// Return a path to redirect there, or null to let the page open.
if (DV.Auth.currentUser == null) return DVRoutes.index.path;
return null;
}Copy code to clipboard
A guard runs before every page in its folder and below.
Return a path to redirect, or null to open the page.
For roles and permissions, set policy on @DVPage. See Authorization.
Put about.loading.dart and about.error.dart beside about.dart. Name the classes after the page function: _aboutPage uses AboutPageLoading and AboutPageError.
Functional
Class
// lib/pages/about.loading.dart
import 'package:flutter/widgets.dart';
import '../dartvel_client/dartvel_client.dart';
@DVFunctionalWidget()
Widget _aboutPageLoading(BuildContext context) => const DVText('Loading');Copy code to clipboard
Functional
Class
// lib/pages/about.error.dart
import 'package:flutter/widgets.dart';
import '../dartvel_client/dartvel_client.dart';
@DVFunctionalWidget()
Widget _aboutPageError(BuildContext context) =>
const DVText('That did not load');Copy code to clipboard
A page without them gets a default spinner and error message.
Partial
Spec section: Error, Empty, and Loading States
Planned work and implementation limits
No page-level error boundary for errors thrown while the page builds.
No per-status-code pages, offline banner or model skeletons.
A path no route serves renders a 404 page by default, in your app's theme, with a link to the home page. You write nothing for that.
To send unknown paths somewhere instead, name where:
# pubspec.yaml
dartvel:
notFoundRedirect: /Copy code to clipboard
It lives at /404. Put a page of your own there and yours is used instead.
For a page of your own anywhere else, put one at the route you redirect to.
On a static host, dartvel build web also writes 404/index.html for paths the app never loads.
See Static web hosting for the Apache rules.
Functional
Class
@DVPage(
title: 'Pricing',
sitemap: DVPageSitemap(
priority: 0.8,
changeFrequency: DVSitemapChangeFrequency.weekly,
),
)
Widget _pricingPage(BuildContext context) => const DVText('Plans');Copy code to clipboard
title becomes the browser title and the page's SEO title.
sitemap sets this page's priority and change frequency in sitemap.xml.
The sitemap is written when dartvel.seo.siteUrl is set.
A _layout.dart that extends DartvelTabsLayout makes its folder a set of tabs. A detail page is pushed inside its tab, and back pops inside the tab on screen.
dartvelRoutes(at: '/app') returns every Dartvel route under a prefix for your own GoRouter. DV.Navigation and DVNavLink place Dartvel targets under it and leave your paths alone.
Each kind of router takes your existing routes as existing:: dartvelGoRouter for go_router, dartvelAutoRoutes for auto_route, dartvelRouteFactory for Navigator 1.0, dartvelNavigator2_0Routes spread into a Navigator 2.0 route table, and dartvelRouterConfig for MaterialApp.router or CupertinoApp.router. Dartvel answers its own paths under the prefix, with its guards, and back goes through Dartvel's stack first. Every other path is handled by your router. dartvel init says which router an app uses.
The generated router is built on
by the Flutter authors (BSD-3-Clause).
List your domains under dartvel.deepLinks in pubspec.yaml. dartvel build web writes assetlinks.json and apple-app-site-association, and dartvel doctor --target android,ios checks the deployed files.
Built
Spec section: Pages
Built
Spec section: Routing
Implementation notes
Typed DVRoutes targets for routes inside DVGoRoutes.
Routing packages other than go_router and auto_route are not tested; they can only join through dartvelRouterConfig.
FSL-1.1-MIT licensed. Built with Dartvel.
Dartvel is made by
To the bottom