unrouter 0.3.0
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 #
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
- Install
- Quick start
- Routing approaches
- Layouts and nested routing
- Route patterns and matching
- Navigation and history
- Navigator 1.0 compatibility
- State and params
- Link widget
- Web URL strategy
- Testing
- API overview
- Example app
- Troubleshooting
- Contributing
- License
Features
- Declarative routes via
Unrouter(routes: ...) - Widget-scoped routes via the
Routeswidget - Hybrid routing (declarative first, widget-scoped fallback)
- Nested routes + layouts (
Outletfor declarative routes,Routesfor 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, defaulttrue) - Web URL strategies:
UrlStrategy.browserandUrlStrategy.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 == ''andchildren.isEmpty - Layout:
path == ''andchildren.isNotEmpty(does not consume segments) - Leaf:
path != ''andchildren.isEmpty - Nested:
path != ''andchildren.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);
Navigation from any widget #
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: .browseruses path URLs like/about(requires server rewrites).strategy: .hashuses 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 toMaterialApp.router)Inlet: route definition (index/layout/leaf/nested)Outlet: renders the next matched child route (declarative routes)Routes: widget-scoped route matcherNavigate: navigation interface (Navigate.of(context))RouterStateProvider: readRouteInformation+ merged paramsHistory/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 anUnrouterrouter (eitherMaterialApp.router(routerConfig: Unrouter(...))orrunApp(Unrouter(...))).Routesrenders nothing: it must be a descendant ofUnrouter(needs aRouterStateProvider).showDialognot working: keepenableNavigator1: true(default).- Web 404 on refresh: use
strategy: .hashor 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.