Skip to content

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 Navigator stack.
  • 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 as PageView, TabBarView, or IndexedStack. 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

  1. Assign the XRouter navigator key to your root app.
  2. Define routes in a <routes> XML file under your values resources.
  3. Navigate with XRouter.goTo(...) from Dart or routeTo(...) from XML.
  4. For PageView, TabBarView, or IndexedStack, define a <routeGroup> and register a callback with XRouter.registerRouteCallback(...) — see Advanced Routing.
  5. Use redirects, presenters, transitions, and nested route groups when the route map needs them.
  6. On web, if you register callback routes, disable Flutter's URL strategy before runApp() — covered in Advanced Routing.

Setup

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:

XRouter.goTo('/settings');  // by path
XRouter.goTo('settings');   // by name
<TextButton onPressed="${routeTo('settings')}">
    <Text data="Open Settings" />
</TextButton>

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:

XRouter.goTo('/');             // resolves to /overview
XRouter.goTo('/old-settings'); // resolves to /settings

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:

XRouter.resolve('/old-settings?tab=billing')?.params;
// {tab: billing}

Parameterized redirect routes can substitute matched path parameters into the redirect target:

<route path="/projects/:id" redirect="/project/:id" />

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:

<route path="/details" fragment="details_panel" presenter="sidePanel" />

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:

XRouter.goTo('/analytics/renders');
XRouter.goTo('analytics:renders');

The nested <routeGroup> counts as one child view in its parent group. With the example above:

  • sideBar index 0 is /overview.
  • sideBar index 1 is the nested analytics group host.
  • sideBar index 2 is /channels.

The child routes inside analytics have their own indexes:

  • analytics index 0 is /analytics/navigation.
  • analytics index 1 is /analytics/renders.
  • analytics index 2 is /analytics/downloads.
  • analytics index 3 is /analytics/errors.

When navigating to /analytics/renders, XRouter activates the parent group first, then the nested group:

  1. Calls the sideBar callback with index 1 and name analytics.
  2. Waits for the analytics callback to be registered if it is not ready yet.
  3. Calls the analytics callback with index 1 and name analytics: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:

  • path must start with /.
  • path cannot contain repeated slashes.
  • path cannot end with /, except for the root path /.
  • Each <route> must define either fragment or redirect, 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:

XRouter.goTo(
  '/project/abc123?tab=settings',
  params: {'source': 'sidebar'},
);

Merge order is path parameters, then query parameters, then caller-provided parameters. Later values override earlier values with the same key.

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

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:

final current = XRouter.currentRoute;

if (current?.path == '/settings') {
  // Already on settings.
}

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.