Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Telegram Mini Apps
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

APP

Routing

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

Add a page with a file

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.

Or declare routes in a file, if you prefer

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.

Write logic in a page body

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.

Read route and query parameters

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

Navigate with typed DVRoutes targets

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/42

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

Link with DVNavLink

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.

Choose when it preloads and previews

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.

Show your own preview

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.

Share chrome with _layout.dart

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

Nest a layout in a folder

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

Guard a folder with _guard.dart

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

Show loading and error states

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.

Handle unknown URLs

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.

Set the title and sitemap entry

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.

Layouts, tabs and somebody else's router

Tabs from a folder

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.

Mount into the router you already have

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

go_router

by the Flutter authors (BSD-3-Clause).

Deep links

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.

Status

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.

PREVIOUS UI and styling DVBox, DVText, modifiers and layouts
NEXT State Signals, derived signals and globals
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom