unrouter 0.5.0 copy "unrouter: ^0.5.0" to clipboard
unrouter: ^0.5.0 copied to clipboard

A Flutter router that gives you routing flexibility: define routes centrally, scope them to widgets, or mix both - with browser-style history navigation.

unrouter #

pub license

A Flutter router that gives you routing flexibility: define routes centrally, scope them to widgets, or mix both - with browser-style history navigation.

Documentation #

All sections below are collapsible. Expand the chapters you need.

Features
  • Declarative routes via Unrouter(routes: ...)
  • Widget-scoped routes via the Routes widget
  • Hybrid routing (declarative first, widget-scoped fallback)
  • Nested routes + layouts (Outlet for declarative routes, Routes for widget-scoped)
  • URL patterns: static, params (:id), optionals (?), wildcard (*)
  • Browser-style navigation: push/replace/back/forward/go
  • Async navigation results via Navigation (awaitable)
  • Navigation guards with allow/cancel/redirect
  • Route blockers for back/pop confirmation
  • Navigator 1.0 compatibility for overlays and imperative APIs (enableNavigator1, default true)
  • Web URL strategies: UrlStrategy.browser and UrlStrategy.hash
  • Relative navigation with dot segment normalization (./, ../)

Install

Add to pubspec.yaml:

dependencies:
  unrouter: ^0.3.0

Quick start

Declarative routing setup:

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

final router = Unrouter(
  strategy: .browser,
  routes: const [
    Inlet(factory: HomePage.new),
    Inlet(path: 'about', factory: AboutPage.new),
    Inlet(
      factory: AuthLayout.new,
      children: [
        Inlet(path: 'login', factory: LoginPage.new),
        Inlet(path: 'register', factory: RegisterPage.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));

Use Unrouter directly as an entry widget (no MaterialApp required):

void main() => runApp(router);

Routing approaches

Declarative routing (central config) #

Unrouter(routes: [
  Inlet(factory: HomePage.new),
  Inlet(path: 'about', factory: AboutPage.new),
])

Widget-scoped routing (component-level) #

Unrouter(child: Routes([
  Inlet(factory: HomePage.new),
  Inlet(path: 'about', factory: AboutPage.new),
]))

Hybrid routing (declarative first, widget-scoped fallback) #

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

Hybrid routing also enables partial matches where a declarative route handles the prefix and a nested Routes widget handles the rest.

Layouts and nested routing

Declarative layouts use Outlet #

Layout and nested routes defined in Unrouter.routes must render an Outlet to show matched children. Layout routes (path == '' with children) do not consume a path segment.

class AuthLayout extends StatelessWidget {
  const AuthLayout({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: Outlet());
  }
}

Widget-scoped nesting uses Routes #

class ProductsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Products')),
      body: Routes([
        Inlet(factory: ProductsList.new),
        Inlet(path: ':id', factory: ProductDetail.new),
        Inlet(path: 'new', factory: NewProduct.new),
      ]),
    );
  }
}

State preservation #

unrouter keeps matched pages in an IndexedStack. Leaf routes are keyed by history index, while layout/nested routes are cached by route identity to keep their state when switching between children. Prefer const routes to maximize reuse.

Route patterns and matching

Pattern syntax #

  • Static: about, users/profile
  • Params: users/:id, :userId
  • Optional: :lang?/about, users/:id/edit?
  • Wildcard: files/*, *

Route kinds #

  • Index: path == '' and children.isEmpty
  • Layout: path == '' and children.isNotEmpty (does not consume segments)
  • Leaf: path != '' and children.isEmpty
  • Nested: path != '' and children.isNotEmpty

Partial matching #

Routes performs greedy matching and allows partial matches so nested component routes can continue to match the remaining path.

Navigation and history

Imperative navigation (shared router instance) #

router.navigate(.parse('/about'));
router.navigate(.parse('/login'), replace: true);
router.navigate.back();
router.navigate.forward();
router.navigate.go(-1);

All navigation methods return Future<Navigation>. You can ignore the result or await it when you need to know what happened.

final result = await router.navigate(.parse('/about'));
if (result case NavigationRedirected()) {
  // handle redirects
}
context.navigate(.parse('/users/123'));
context.navigate(.parse('edit'));        // /users/123/edit
context.navigate(.parse('./edit'));      // /users/123/edit
context.navigate(.parse('../settings')); // /users/123/settings

Context extensions #

context.navigate(.parse('/about'));
final router = context.router;

Relative navigation #

Relative paths append to the current location and normalize dot segments. Query and fragment come from the provided URI and do not inherit.

Building paths #

unrouter uses Uri as the first-class navigation input. You can build paths directly with templates or Uri helpers:

final id = '123';
final uri = Uri.parse('/users/$id');
final withQuery = Uri(path: '/users/$id', queryParameters: {'tab': 'profile'});
context.navigate(withQuery);

Navigation guards

Guards let you intercept navigation and decide whether to allow, cancel, or redirect.

final router = Unrouter(
  routes: const [Inlet(factory: HomePage.new)],
  guards: [
    (context) {
      if (!auth.isSignedIn) {
        return GuardResult.redirect(Uri.parse('/login'));
      }
      return GuardResult.allow;
    },
  ],
);

You can also attach guards to specific declarative routes:

final routes = [
  Inlet(
    path: 'admin',
    guards: [
      (context) => GuardResult.redirect(Uri.parse('/login')),
    ],
    factory: AdminPage.new,
  ),
];

Guards run in order: global guards first, then matched route guards from root to leaf. The first non-allow result (cancel/redirect) short-circuits.

Guards receive a GuardContext:

  • to: target RouteInformation
  • from: previous RouteInformation
  • replace: whether the navigation is a replace
  • redirectCount: number of redirects so far

You can return a Future<GuardResult> for async checks, and configure maxRedirects to prevent redirect loops.

Route blockers

Route blockers intercept back/pop navigation (history go(-1) / back) and let you confirm before leaving the current route. They run from child to parent.

class SettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RouteBlocker(
      onWillPop: (ctx) async {
        final result = await showDialog<bool>(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('Discard changes?'),
            actions: [
              TextButton(
                onPressed: () => Navigator.of(context).pop(false),
                child: const Text('Cancel'),
              ),
              TextButton(
                onPressed: () => Navigator.of(context).pop(true),
                child: const Text('Discard'),
              ),
            ],
          ),
        );
        return result == true;
      },
      onBlocked: (ctx) {
        // Optional: record analytics or show a toast.
      },
      child: const SettingsForm(),
    );
  }
}

Notes:

  • Blockers only apply to history back/pop (navigate.back() / navigate.go(-1)).
  • navigate.go(0) also triggers blockers.
  • forward does not trigger blockers.
  • Navigator.pop is still handled by Flutter's WillPopScope/PopScope.

Route animations

unrouter provides a per-route AnimationController you can use to animate incoming/outgoing pages without relying on Navigator 1.0.

class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final animation = context.routeAnimation(
      duration: const Duration(milliseconds: 300),
    );
    return FadeTransition(
      opacity: animation,
      child: const Text('Profile'),
    );
  }
}

The controller runs forward on push/replace and reverse on pop. Pages that don't call routeAnimation behave as they do today. The default duration is 300ms unless you provide one.

Navigator 1.0 compatibility

By default, Unrouter embeds a Navigator so APIs like showDialog, showModalBottomSheet, showGeneralDialog, showMenu, and Navigator.push/pop/popUntil work as expected.

final router = Unrouter(
  enableNavigator1: true, // default
  routes: const [Inlet(factory: HomePage.new)],
);

Set enableNavigator1: false to keep the Navigator 2.0-only behavior.

State and params
final state = context.routeState;
final uri = state.location.uri;
final params = state.params;        // merged params up to this level
final extra = state.location.state; // history entry state (if any)

You can also read fine-grained fields (with narrower rebuild scopes):

final location = context.location;
final matched = context.matchedRoutes;
final params = context.params;
final level = context.routeLevel;
final index = context.historyIndex;
final action = context.historyAction;

RouteState.action tells you whether the current navigation was a push, replace, or pop, and historyIndex can be used to reason about stacked pages.

Link widget

Basic link:

Link(
  to: Uri.parse('/about'),
  child: const Text('About'),
)

Custom link with builder:

Link(
  to: Uri.parse('/products/1'),
  state: {'source': 'home'},
  builder: (context, location, navigate) {
    return GestureDetector(
      onTap: () => navigate(),
      onLongPress: () => navigate(replace: true),
      child: Text('Product 1'),
    );
  },
)

Web URL strategy
  • strategy: .browser uses path URLs like /about (requires server rewrites).
  • strategy: .hash uses hash URLs like /#/about (no rewrites required).

UrlStrategy only applies to Flutter web. On native platforms (Android/iOS/macOS/Windows/Linux), Unrouter uses MemoryHistory by default. If you pass a custom history, strategy is ignored.

Testing

MemoryHistory makes routing tests easy:

final router = Unrouter(
  routes: const [
    Inlet(factory: HomePage.new),
    Inlet(path: 'about', factory: AboutPage.new),
  ],
  history: MemoryHistory(
    initialEntries: [RouteInformation(uri: Uri.parse('/about'))],
  ),
);

Run tests:

flutter test

API overview
  • Unrouter: widget + RouterConfig<RouteInformation> (use directly or pass to MaterialApp.router)
  • Inlet: route definition (index/layout/leaf/nested)
  • Outlet: renders the next matched child route (declarative routes)
  • Routes: widget-scoped route matcher
  • Navigate: navigation interface (context.navigate)
  • Navigation: async result returned by navigation methods
  • Guard / GuardResult: navigation interception and redirects
  • RouteState: current route state (read via context.routeState)
  • History / MemoryHistory: injectable history (great for tests)
  • Link: declarative navigation widget

Example app

See example/ for a complete Flutter app showcasing routing patterns and Navigator 1.0 APIs.

cd example
flutter run

Troubleshooting
  • context.navigate throws: ensure your widget is under an Unrouter router (either MaterialApp.router(routerConfig: Unrouter(...)) or runApp(Unrouter(...))).
  • Routes renders nothing: it must be a descendant of Unrouter.
  • showDialog not working: keep enableNavigator1: true (default).
  • Web 404 on refresh: use strategy: .hash or configure server rewrites.

Contributing
  • Format: dart format .
  • Tests: flutter test
  • Open a PR with a clear description and a focused diff

License

MIT - see LICENSE.

2
likes
160
points
84
downloads

Publisher

verified publishermedz.dev

Weekly Downloads

A Flutter router that gives you routing flexibility: define routes centrally, scope them to widgets, or mix both - with browser-style history navigation.

Repository (GitHub)
View/report issues

Topics

#router #routing #navigation

Documentation

API reference

License

MIT (license)

Dependencies

flutter, meta, web

More

Packages that depend on unrouter