unrouter

Unrouter

The flexible Flutter router that adapts to your architecture

pub tests license


Overview

Unrouter is a production-ready Flutter router that supports declarative, widget-scoped, and hybrid routing. It provides browser-style history, async guards, route blockers, and Navigator 1.0 compatibility while keeping route definitions flexible and readable.

Highlights:

  • Declarative routes with Inlet, widget-scoped routes with Routes, or both
  • Browser-style navigation (push/replace/back/forward/go)
  • Async guards and route blockers (allow/cancel/redirect)
  • Nested layouts with Outlet and infinite depth
  • Named routes with URI generation
  • Optional file-based routing CLI (init/scan/generate/watch)
  • Web URL strategies (browser/hash) and history state

https://github.com/user-attachments/assets/e4f2d9d1-3fe2-4050-8b5b-1e1171027ba2

Installation

Run this command:

flutter pub add unrouter

Quick start

Minimal setup

import 'package:flutter/material.dart';
import 'package:unrouter/unrouter.dart';

void main() => runApp(
  Unrouter(
    routes: const [
      Inlet(name: 'home', factory: HomePage.new),
      Inlet(name: 'about', path: 'about', factory: AboutPage.new),
    ],
  ),
);

With MaterialApp

final router = Unrouter(
  strategy: .browser,
  routes: const [
    Inlet(factory: HomePage.new),
    Inlet(path: 'about', factory: AboutPage.new),
    Inlet(
      path: 'users',
      factory: UsersLayout.new,
      children: [
        Inlet(factory: UsersIndexPage.new),
        Inlet(path: ':id', factory: UserDetailPage.new),
      ],
    ),
    Inlet(path: '*', factory: NotFoundPage.new),
  ],
);

void main() => runApp(MaterialApp.router(routerConfig: router));
context.navigate(path: '/about');
context.navigate(name: 'userDetail', params: {'id': '123'});
context.navigate.back();

context.navigate(path: 'edit');         // /users/123/edit
context.navigate(path: './edit');       // /users/123/edit
context.navigate(path: '../settings');  // /users/settings

Core concepts

Unrouter

Unrouter is a RouterConfig you can pass to MaterialApp.router or use as a standalone widget. Provide either routes, child, or both.

Inlet

An Inlet defines a route segment and optional children.

Inlet(
  name: 'userDetail',
  path: 'users/:id',
  factory: UserDetailPage.new,
)

Routes and Outlet

Routes enables widget-scoped routing. Outlet renders matched child routes.

class UsersLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: const [
        UsersToolbar(),
        Expanded(child: Outlet()),
      ],
    );
  }
}

Routing approaches

Declarative

Unrouter(
  routes: const [
    Inlet(path: 'admin', factory: AdminPage.new),
  ],
)

Widget-scoped

Routes([
  Inlet(factory: HomePage.new),
  Inlet(path: 'settings', factory: SettingsPage.new),
])

Hybrid

Unrouter(
  routes: const [
    Inlet(path: 'admin', factory: AdminPage.new),
  ],
  child: Routes([
    Inlet(factory: HomePage.new),
  ]),
)

Route patterns

Supported path tokens:

  • Static segments: about
  • Named params: users/:id
  • Optional segments: :id?
  • Wildcards: *, *name (catch-all)
Inlet(path: 'users/:id', factory: UserDetailPage.new);
Inlet(path: 'blog/:slug?', factory: BlogPage.new);
Inlet(path: 'docs/*path', factory: DocsPage.new);
Inlet(path: '*', factory: NotFoundPage.new);

Named routes let you generate URIs and navigate by name:

Inlet(name: 'userDetail', path: 'users/:id', factory: UserDetailPage.new);

Layouts and nested routing

Layout routes (path == '')

Inlet(
  factory: AuthLayout.new,
  children: [
    Inlet(path: 'login', factory: LoginPage.new),
    Inlet(path: 'register', factory: RegisterPage.new),
  ],
)

Nested routes (path + children)

Inlet(
  path: 'users',
  factory: UsersLayout.new,
  children: [
    Inlet(factory: UsersIndexPage.new),
    Inlet(path: ':id', factory: UserDetailPage.new),
  ],
)
context.navigate(
  name: 'userDetail',
  params: {'id': '123'},
  query: {'tab': 'posts'},
  fragment: 'latest',
);

context.navigate(path: '/about', replace: true);

Generate a URI

final uri = context.navigate.route(
  name: 'userDetail',
  params: {'id': '123'},
  query: {'tab': 'posts'},
);

History controls

context.navigate.back();
context.navigate.forward();
context.navigate.go(-2);

Navigation calls return Future<Navigation> so you can detect allow/cancel/redirect.

File-based routing (CLI)

Unrouter ships a CLI to scan a pages directory and generate a routes file.

1) Create config (optional)

Create unrouter.config.dart in your project root (the CLI scans upward from current working directory). The CLI reads this file with the analyzer and does not execute it.

// unrouter.config.dart
const pagesDir = 'lib/pages';
const output = 'lib/routes.dart';

Notes:

  • Both values are optional.
  • Paths can be absolute or relative to unrouter.config.dart.
  • CLI flags (--pages, --output) override the config file.
  • If no config file is found, the CLI uses the nearest pubspec.yaml as the root.

2) File to route conventions

  • index.dart maps to the directory root.
  • [id].dart maps to a named parameter (:id).
  • [...path].dart maps to a wildcard (*path) and exposes path in params.
  • Group segments in parentheses (e.g. (auth)) are ignored in the URL path.
  • (group).dart creates a pathless layout for that group; if it is missing, the group is purely organizational.
  • Folder segments map to path segments, and index.dart becomes the parent path.

Examples:

lib/pages/index.dart                  -> /
lib/pages/about.dart                  -> /about
lib/pages/users/index.dart            -> /users
lib/pages/users/[id].dart             -> /users/:id
lib/pages/docs/[...path].dart         -> /docs/*path
lib/pages/(auth)/login.dart           -> /login
lib/pages/(auth).dart                 -> / (pathless layout)
lib/pages/(marketing)/about.dart      -> /about

If a path segment has both a file and children, the children are generated as nested routes. For example:

lib/pages/users/[id].dart
lib/pages/users/[id]/settings.dart

Generates a nested tree equivalent to:

Inlet(
  path: 'users/:id',
  factory: UserDetailPage.new,
  children: [
    Inlet(path: 'settings', factory: UserSettingsPage.new),
  ],
);

If both users.dart and users/index.dart exist, users.dart becomes the parent route and users/index.dart becomes its index child (path: '').

3) Add metadata (optional)

You can add page-level metadata to influence generated routes:

// lib/pages/users/[id].dart
import 'package:unrouter/unrouter.dart';

Future<GuardResult> authGuard(GuardContext context) async {
  return GuardResult.allow;
}

const route = RouteMeta(
  name: 'userDetail',
  guards: const [authGuard],
);

If name or guards are not literals, the generator falls back to route.name / route.guards when building Inlets.

4) Use the generated routes

import 'package:unrouter/unrouter.dart';
import 'routes.dart';

final router = Unrouter(
  routes: routes,
);

The generator picks the widget class for a page file by:

  1. Prefer class names ending in Page or Screen.
  2. Otherwise, use the first class that extends a Widget type.

5) Generate routes

  • unrouter generate (one-time build)
  • unrouter watch (rebuild on changes)

Use --verbose on generate to print a detailed route table.

CLI options

Global options (all commands):

  • -p, --pages Pages directory (default: lib/pages)
  • -o, --output Generated file path (default: lib/routes.dart)
  • --no-color Disable ANSI colors (also respects NO_COLOR)
  • -h, --help Show usage

Command options:

  • scan: -q, --quiet, --json
  • init: -f, --force, -q, --quiet
  • generate: -v, --verbose, -q, --quiet, --json
  • watch: -q, --quiet

Guards

Guards run from root to leaf and can allow, cancel, or redirect navigation.

Future<GuardResult> authGuard(GuardContext context) async {
  if (!auth.isSignedIn) {
    return GuardResult.redirect(name: 'login');
  }
  return GuardResult.allow;
}

Unrouter(
  guards: [authGuard],
  routes: const [
    Inlet(path: 'login', factory: LoginPage.new),
    Inlet(path: 'admin', factory: AdminPage.new),
  ],
)

Route blockers

Use RouteBlocker to intercept back/pop events and confirm navigation.

RouteBlocker(
  onWillPop: (context) async => !await confirmLeave(),
  child: Routes([
    Inlet(factory: EditPage.new),
  ]),
)

Link renders a tappable widget that navigates on click/tap and supports named routes, paths, params, query, and fragment.

Link(
  name: 'userDetail',
  params: const {'id': '123'},
  child: const Text('View profile'),
)

Route animations

Access per-route animation controllers:

final animation = context.routeAnimation();

Enable the embedded Navigator 1.0 for dialogs, bottom sheets, and other Navigator APIs:

Unrouter(
  enableNavigator1: true,
  routes: const [...],
)

Web URL strategy

Unrouter(
  strategy: .browser, // or .hash
  routes: const [...],
)

Use hash strategy when you cannot configure server rewrites.

State restoration

MaterialApp.router(
  routerConfig: router,
  restorationScopeId: 'unrouter',
)

Testing

flutter test

Example app

cd example
flutter run

Contributing

git clone https://github.com/medz/unrouter.git
cd unrouter
flutter pub get
dart format .
flutter analyze
flutter test

Follow flutter_lints and keep changes focused.

License

MIT License - see LICENSE for details.

Built with ❤️ by Seven Du