Routing
XRouter is XWidget's route resolver and navigation layer. It maps URL-style paths or route names to XML fragments, then resolves the target into one of these behaviors:
- Navigator routes inflate the route's fragment and push it onto Flutter's
Navigatorstack. - Presenter routes inflate the route's fragment into an overlay such as a dialog or bottom sheet.
- Callback routes call a registered route callback for a
<routeGroup>. Use these for multi-page widgets such asPageView,TabBarView, orIndexedStack. See Advanced Routing. - Redirect routes resolve one route target to another before navigation.
Navigation is context-free. Controllers, callbacks, and XML expressions can
all navigate without a BuildContext because XRouter uses
XRouter.navigatorKey for Navigator-backed pages.
XRouter.goTo('/settings');
XRouter.goTo('/analytics/renders');
XRouter.goTo('/login', action: NavigatorAction.pushAndRemoveAll);
<TextButton onPressed="${routeTo('/settings')}">
<Text data="Settings" />
</TextButton>
<ListTile onTap="${routeTo('/analytics/renders')}">
<Text for="title" data="Renders" />
</ListTile>
<ElevatedButton onPressed="${routeTo('/login', 'pushAndRemoveAll')}">
<Text data="Log Out" />
</ElevatedButton>
Quick Start
- Assign the XRouter navigator key to your root app.
- Define routes in a
<routes>XML file under your values resources. - Navigate with
XRouter.goTo(...)from Dart orrouteTo(...)from XML. - For
PageView,TabBarView, orIndexedStack, define a<routeGroup>and register a callback withXRouter.registerRouteCallback(...)— see Advanced Routing. - Use redirects, presenters, transitions, and nested route groups when the route map needs them.
- On web, if you register callback routes, disable Flutter's URL strategy
before
runApp()— covered in Advanced Routing.
Setup
Navigator Key
Navigator routes are pushed through Flutter's Navigator. Add
XRouter.navigatorKey to your root MaterialApp or CupertinoApp:
<MaterialApp
xmlns="https://xwidget.dev/fragments"
title="My App"
navigatorKey="${navigatorKey()}">
...
</MaterialApp>
navigatorKey() is registered during XWidget.initialize() and returns
XRouter.navigatorKey. If the key is not assigned, XRouter can still resolve
routes, but Navigator-backed navigation and presenter routes will not have a
root context to use.
Route File
Routes are value resources. Place a single XML file with a <routes> root in
your values directory, typically resources/values/routes.xml:
<routes xmlns="https://xwidget.dev/routes" maxRedirects="3">
<route path="/" redirect="/overview" />
<route path="/login" fragment="login" transition="fade" />
<route path="/about" fragment="about_dialog" presenter="dialog" />
<routeGroup name="main">
<route path="/overview" fragment="pages/overview" name="overview" />
<routeGroup path="/analytics" name="analytics" fragment="analytics">
<route path="/renders" fragment="analytics/renders" name="renders" />
<route path="/errors" fragment="analytics/errors" name="errors" />
</routeGroup>
<route path="/settings" fragment="pages/settings" name="settings" />
</routeGroup>
</routes>
Route files are loaded by the standard resource system during
XWidget.initialize(). Local resources, hot reload, and XWidget Cloud updates
all feed the same router map.
Use one <routes> file per loaded resource bundle. When route XML is reloaded,
XRouter replaces the previous routes for that source before registering the new
definitions.
Defining Routes
Standalone Routes
A standalone route maps one path to one fragment:
<routes>
<route path="/login" fragment="login" />
<route path="/settings" fragment="settings" name="settings" />
</routes>
path is required. A route must define either fragment or redirect.
name is optional and creates an additional lookup key:
Route Attributes
| Attribute | Required | Description |
|---|---|---|
path |
Yes | URL path. Must begin with /. |
fragment |
Required unless redirect is present |
Fragment to inflate when this route is active. |
redirect |
Required unless fragment is present |
Target route to resolve instead of rendering this route. |
name |
No | Programmatic lookup key for XRouter.goTo(...). |
history |
No | Whether web URL sync should push this route into browser history. Defaults to true, or inherits from the group. |
presenter |
No | Presenter used to open the fragment as an overlay. |
transition |
No | Navigator page transition used when pushing the fragment. |
fragment and redirect are mutually exclusive.
Redirects
Use redirect when a route should resolve to another route:
<routes xmlns="https://xwidget.dev/routes" maxRedirects="3">
<route path="/" redirect="/overview" />
<route path="/old-settings" redirect="/settings" />
<route path="/overview" fragment="overview" />
<route path="/settings" fragment="settings" />
</routes>
Redirect targets must begin with /. XRouter follows redirects during
resolution, so callers still navigate normally:
maxRedirects on the root <routes> element limits redirect chains. If the
limit is exceeded, XRouter throws instead of looping forever. The default is
5.
When a path with query parameters redirects, the query string is carried to the final target:
Parameterized redirect routes can substitute matched path parameters into the redirect target:
Presenters
Use presenter when a route should open as an overlay instead of pushing a new
Navigator page:
<route path="/about" fragment="about_dialog" presenter="dialog" />
<route path="/share" fragment="share_sheet" presenter="bottomSheet" />
Built-in presenters are:
| Presenter | Behavior |
|---|---|
dialog |
Opens the fragment with showDialog(...). |
bottomSheet |
Opens the fragment with showModalBottomSheet(...). |
Register custom presenters in Dart:
XRouter.registerPresenter('sidePanel', (route, dependencies, params) {
// Open route.fragment however your app presents side panels.
});
Then reference the presenter by name:
Presenter routes still resolve like any other route and receive merged
parameters. They do not become XRouter.currentRoute and do not push browser
history entries, because XRouter does not own the overlay route stack.
Transitions
Use transition to choose the page transition for Navigator-backed routes:
<route path="/login" fragment="login" transition="fade" />
<route path="/settings" fragment="settings" transition="slide" />
Built-in transition values are:
| Transition | Behavior |
|---|---|
none |
No animation. |
fade |
Fade in from transparent. |
slide |
Slide in from the right. |
scale |
Scale in. |
cupertino |
Uses CupertinoPageRoute. |
material |
Uses the default MaterialPageRoute behavior. |
Omitting transition also uses the default Material route behavior.
Route Groups
A route group collects related routes under a shared group name and, optionally, a shared path prefix:
<routes>
<routeGroup name="analytics" path="/analytics">
<route path="/overview" fragment="analytics/overview" name="overview" />
<route path="/renders" fragment="analytics/renders" name="renders" />
<route path="/downloads" fragment="analytics/downloads" name="downloads" />
<route path="/errors" fragment="analytics/errors" name="errors" />
</routeGroup>
</routes>
With that definition, these targets resolve to the renders route:
XRouter.goTo('/analytics/renders'); // full path
XRouter.goTo('analytics:renders'); // group-qualified name
Direct group route behavior is determined at runtime:
- If the group has a registered callback, navigation invokes that callback.
- If the group does not have a registered callback, navigation falls back to Navigator behavior and pushes the route's fragment.
Nested route group navigation uses the activation cascade described below, so XRouter waits for the required parent and child callbacks instead of pushing a partially nested route as a standalone page.
The XML structure is the same for both cases.
Group Attributes
| Attribute | Required | Description |
|---|---|---|
name |
Yes | Identifies the route group and prefixes child route names. |
path |
No | Shared path prefix. A group path of /analytics plus child path /renders becomes /analytics/renders. |
history |
No | Default browser-history behavior for child routes. Defaults to true. |
fragment |
No | Optional fragment name for a nested group's host view. Parent callbacks still select by index and name. |
presenter |
No | Default presenter inherited by child routes and nested groups. |
transition |
No | Default transition inherited by child routes and nested groups. |
Child route names are qualified with the group name. A child route with
name="renders" inside name="analytics" is registered as
analytics:renders. If the child route has no name, it can still be
navigated by path, but it has no group-qualified name.
Group-level history, presenter, and transition values are inherited by
child routes. Child routes and nested groups can override them.
Nested Route Groups
A <routeGroup> can contain another <routeGroup>. This is useful when a
parent navigation surface contains a child view that has its own internal
navigation, such as a sidebar page that hosts an analytics PageView:
<routes xmlns="https://xwidget.dev/routes">
<routeGroup name="sideBar">
<route path="/overview" name="overview" fragment="analytics/overview" />
<routeGroup path="/analytics" name="analytics" fragment="analytics">
<route path="/navigation" name="navigation" fragment="analytics/navigation" />
<route path="/renders" name="renders" fragment="analytics/renders" />
<route path="/downloads" name="downloads" fragment="analytics/downloads" />
<route path="/errors" name="errors" fragment="analytics/errors" />
</routeGroup>
<route path="/channels" name="channels" fragment="channels" />
</routeGroup>
</routes>
Nested group paths are joined with the parent path prefix. In this example,
/analytics plus /renders becomes /analytics/renders.
Nested group names are scoped to the nested group itself. The renders route can be reached by path or by the nested group-qualified name:
The nested <routeGroup> counts as one child view in its parent group. With the
example above:
sideBarindex0is/overview.sideBarindex1is the nestedanalyticsgroup host.sideBarindex2is/channels.
The child routes inside analytics have their own indexes:
analyticsindex0is/analytics/navigation.analyticsindex1is/analytics/renders.analyticsindex2is/analytics/downloads.analyticsindex3is/analytics/errors.
When navigating to /analytics/renders, XRouter activates the parent group
first, then the nested group:
- Calls the
sideBarcallback with index1and nameanalytics. - Waits for the
analyticscallback to be registered if it is not ready yet. - Calls the
analyticscallback with index1and nameanalytics:renders.
This same cascade is used for startup deep links and browser back/forward navigation, so a direct browser URL can land inside a nested route group after the relevant controllers register their callbacks.
Default Route and Group Resume
The first direct route in a group is the group's default. XRouter aliases both the group name and the group path to that route:
<routeGroup name="main" path="/app">
<route path="/dashboard" fragment="dashboard" name="dashboard" />
<route path="/settings" fragment="settings" name="settings" />
</routeGroup>
All three targets resolve to the dashboard route:
XRouter.goTo('/app/dashboard'); // full path
XRouter.goTo('/app'); // group path alias
XRouter.goTo('main'); // group name alias
After a group has an active route, navigating to the group alias resumes the
last active child route instead of resetting to the default. For example, if
main:settings was the last active route, XRouter.goTo('main') returns to
settings.
For web apps whose primary callback route group owns the first screen, map that
group to /:
<routeGroup name="main" path="/">
<route path="/overview" fragment="overview" name="overview" />
<route path="/settings" fragment="settings" name="settings" />
</routeGroup>
The group path / becomes an alias for the first route, so / resolves to
/overview. This gives the browser's initial history entry a valid route in
apps where the main view is callback-driven.
Path Rules
Route paths follow a small set of rules enforced by the parser:
pathmust start with/.pathcannot contain repeated slashes.pathcannot end with/, except for the root path/.- Each
<route>must define eitherfragmentorredirect, but not both. - Redirect targets must start with
/and follow the same slash rules. - Route paths, names, and aliases must be unique.
Grouped route paths are normalized when the group prefix and child path are
joined, so path="/" plus path="/overview" becomes /overview.
Path Parameters
Use :name in a path segment to declare a path parameter:
<route path="/project/:projectId" fragment="project/detail" />
<route path="/project/:projectId/channel/:channelId" fragment="project/channel" />
final route = XRouter.resolve('/project/abc123/channel/stable');
print(route?.fragment); // project/channel
print(route?.params); // {projectId: abc123, channelId: stable}
Resolution checks exact paths first, then parameterized patterns in registration order. Avoid ambiguous parameterized routes where two patterns could match the same URL.
ResolvedRoute.path stores the route definition path. For a parameterized
route, that is the pattern, such as /project/:projectId, while the extracted
values live in ResolvedRoute.params.
Query Parameters
Query parameters are parsed and merged into the same parameter map:
final route = XRouter.resolve('/project/abc123?tab=settings');
print(route?.params); // {projectId: abc123, tab: settings}
Names and aliases are matched directly. Query strings are parsed only when the
target starts with /, http://, or https://.
On web, URL sync preserves query strings during normal navigation, startup deep links, and browser back/forward navigation.
XRouter is not an external URL launcher. Absolute URLs are parsed with
Uri.parse(...), and only their path and query string are used for route
resolution.
Caller-Provided Parameters
goTo accepts a params map. These values are merged with path and query
parameters before the route is delivered to the fragment or callback:
Merge order is path parameters, then query parameters, then caller-provided parameters. Later values override earlier values with the same key.
Navigation
Dart API
Use XRouter.goTo(...) to resolve and navigate in one call:
XRouter.goTo('/analytics/renders');
XRouter.goTo('analytics:renders');
XRouter.goTo('/project/abc123', params: {'tab': 'settings'});
XRouter.goTo('/login', action: NavigatorAction.pushAndRemoveAll);
If no route matches, XRouter logs a warning and does not navigate.
For redirect routes, goTo resolves the final target before navigating. For
Navigator routes, it inflates the target fragment and pushes it through
XRouter.navigateToFragment(...). For presenter routes, it calls the registered
presenter. For callback routes, it invokes the group's registered callback with
the route index, route name, and merged parameters.
Every successful goTo call records a navigation event through
Analytics.trackNavigation(pageName: resolved.path).
Navigator Actions
The action parameter controls Navigator-backed routes:
| Action | Behavior |
|---|---|
NavigatorAction.push |
Pushes the route onto the stack. This is the default. |
NavigatorAction.pushReplacement |
Replaces the current route. |
NavigatorAction.pushAndRemoveAll |
Clears the stack and pushes the new route. Use this for transitions such as login to home. |
These actions are ignored for callback routes because callback routes do not use
Flutter's Navigator.
NavigatorAction.pushAndRemoveUntil exists on the shared enum, but XRouter does
not expose a removeUntil predicate. Do not use that action with XRouter.goTo;
call XRouter.navigateToFragment(...) directly if you need a custom predicate.
XML Navigation
XWidget registers EL helpers during XWidget.initialize().
Use routeTo(target, [action]) when an XML attribute expects a callback:
<TextButton onPressed="${routeTo('/analytics/renders')}">
<Text data="View Renders" />
</TextButton>
<ListTile onTap="${routeTo('/settings')}">
<Text for="title" data="Settings" />
</ListTile>
<ElevatedButton onPressed="${routeTo('/login', 'pushAndRemoveAll')}">
<Text data="Log Out" />
</ElevatedButton>
The optional action string is parsed as a NavigatorAction value. If the
string is omitted or does not match an enum value, XRouter uses
NavigatorAction.push.
routePop() and routePopAll() are intended for Navigator-backed pages only.
They operate on the Navigator stack and do not change the active page in a
callback route group.
<TextButton onPressed="${routePop()}">
<Text data="Go Back" />
</TextButton>
<TextButton onPressed="${routePopAll()}">
<Text data="Return to Root" />
</TextButton>
Resolve Without Navigating
Use XRouter.resolve(...) to inspect a target without navigating:
final route = XRouter.resolve('/analytics/renders');
if (route != null) {
print(route.path); // /analytics/renders
print(route.fragment); // analytics/renders
print(route.name); // analytics:renders
print(route.groupName); // analytics
print(route.viewIndex); // 1
print(route.params); // {}
print(route.transition); // inherited or route-level transition, if any
print(route.presenter); // presenter name, if any
}
XRouter.currentRoute stores the most recently resolved route passed to
goTo(...) for managed Navigator and callback routes:
Presenter routes are overlays and do not update currentRoute.
Analytics
Every successful XRouter.goTo(...) call records the resolved route path with
Analytics.trackNavigation. No additional routing setup is required beyond the
analytics configuration for your resource provider.
See the Analytics documentation for collection and reporting details.