unrouter 0.3.0 copy "unrouter: ^0.3.0" to clipboard
unrouter: ^0.3.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
  • 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);
final nav = Navigate.of(context);
nav(.parse('/users/123'));
nav(.parse('edit'));        // /users/123/edit
nav(.parse('./edit'));      // /users/123/edit
nav(.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);

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 = RouterStateProvider.of(context);
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)

RouterState.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.builder(
  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 (Navigate.of(context))
  • RouterStateProvider: read RouteInformation + merged params
  • 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
  • Navigate.of() 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 (needs a RouterStateProvider).
  • 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.

5
likes
0
points
499
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

License

unknown (license)

Dependencies

flutter, meta, web

More

Packages that depend on unrouter