Declarative, composable router for Flutter.
Features
- 🧩 Nested Routes — Define route trees with
Inletand render child views viaOutlet - 🏷️ Named Routes — Navigate by route name with params, query, and state
- 🛡️ Guards — Navigation-time guards for allow/block/redirect decisions
- 📦 Route Meta — Attach arbitrary metadata to each route, inherited by children
- 🔗 Dynamic Params & Wildcards —
:idparams and*catch-all segments - 🔍 Query Params — First-class
URLSearchParamssupport - 📍 History API —
push,replace,pop,back,forward,go(delta) - ⚡ Reactive Hooks —
useRouter,useLocation,useRouteParams,useQuery,useRouteMeta,useRouteState,useFromLocation
Quick Start
Install
dependencies:
unrouter: <latest>
flutter pub add unrouter
Define Routes
import 'package:flutter/material.dart';
import 'package:unrouter/unrouter.dart';
final authGuard = defineGuard((context) async {
final token = context.query.get('token');
if (token == 'valid') {
return const GuardResult.allow();
}
return GuardResult.redirect('login');
});
final router = createRouter(
guards: [authGuard],
maxRedirectDepth: 8,
routes: [
Inlet(name: 'landing', path: '/', view: LandingView.new),
Inlet(name: 'login', path: '/login', view: LoginView.new),
Inlet(
path: '/workspace',
view: WorkspaceLayoutView.new,
children: [
Inlet(name: 'workspaceHome', path: '', view: DashboardView.new),
Inlet(name: 'profile', path: 'users/:id', view: ProfileView.new),
Inlet(name: 'search', path: 'search', view: SearchView.new),
],
),
Inlet(name: 'docs', path: '/docs/*', view: DocsView.new),
],
);
routes supports multiple top-level Inlets. Use a single parent Inlet with Outlet only when views share the same layout.
For concise route definitions, prefer constructor tear-offs such as MyView.new.
Bootstrap the App
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: createRouterConfig(router),
);
}
}
Render Nested Views with Outlet
class LayoutView extends StatelessWidget {
const LayoutView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: const Outlet(),
);
}
}
Runnable Example
Run the full example app:
cd example
flutter pub get
flutter run -d chrome
Source entry points:
- Quickstart:
example/lib/quickstart/quickstart_app.dart - Advanced:
example/lib/advanced/advanced_app.dart
Core Concepts
Inlet — Route Definition
Inlet is the route-tree building block. Each Inlet describes a path segment, a view builder, optional children, guards, meta, and an optional route name.
Inlet(
name: 'profile',
path: 'users/:id',
view: ProfileView.new,
meta: const {'title': 'Profile', 'requiresAuth': true},
guards: [authGuard],
children: [/* nested Inlets */],
)
| Property | Type | Description |
|---|---|---|
path |
String |
URI path segment pattern. Defaults to '/' |
view |
ViewBuilder |
() => Widget factory, typically MyView.new |
name |
String? |
Route name alias for navigation APIs |
meta |
Map<String, Object?>? |
Route metadata, merged with parent meta |
guards |
Iterable<Guard> |
Route-level guard chain |
children |
Iterable<Inlet> |
Nested child routes |
Guard
A guard runs before navigation is committed and returns one of three outcomes:
GuardResult.allow()GuardResult.block()GuardResult.redirect(pathOrName, {params, query, state})
final adminGuard = defineGuard((context) async {
final isAdmin = context.query.get('role') == 'admin';
if (isAdmin) {
return const GuardResult.allow();
}
return GuardResult.redirect(
'login',
query: URLSearchParams({'from': 'admin'}),
);
});
Guard order is: global → parent → child.
- Redirects are re-validated by guards.
- Redirect commits use
replace. - Redirect depth is capped by
maxRedirectDepth(default8) to prevent infinite loops.
GuardContext
GuardContext provides navigation details:
from/to(HistoryLocation)action(HistoryAction.push,.replace,.pop)params(RouteParams)query(URLSearchParams)meta(Map<String, Object?>)state(Object?)
Named Navigation
push/replace(pathOrName) resolves in this order:
- Try route name first
- If missing, fallback to absolute path
final router = useRouter(context);
await router.push('profile', params: {'id': '42'});
await router.push('/users/42?tab=posts');
await router.replace('landing');
Query Merging
If both the input string and query argument contain query params, they are merged and explicit query entries override same-name keys.
await router.push(
'/search?q=old&page=1',
query: URLSearchParams({'q': 'flutter'}),
);
// => /search?q=flutter&page=1
Link Component
Link is a lightweight widget that triggers navigation.
Link(
to: 'profile',
params: const {'id': '42'},
child: const Text('Open Profile'),
)
Supported props:
toparamsquerystatereplaceenabledonTapchild
Outlet — Nested View Rendering
Outlet renders the matched child view inside its parent. Every level of nesting requires an Outlet in the parent widget tree.
Route Meta
Meta is merged from parent to child routes.
Inlet(
view: Layout.new,
meta: const {'layout': 'dashboard'},
children: [
Inlet(
path: 'admin',
view: AdminView.new,
meta: const {'title': 'Admin', 'requiresAuth': true},
),
],
)
Read meta in a widget:
final meta = useRouteMeta(context);
History Control
final router = useRouter(context);
await router.pop();
router.back();
router.forward();
router.go(-2);
router.go(1);
Reactive Hooks
| Hook | Returns | Description |
|---|---|---|
useRouter(context) |
Unrouter |
Router instance |
useLocation(context) |
HistoryLocation |
Current location (uri + state) |
useRouteParams(context) |
RouteParams |
Matched :param values |
useQuery(context) |
URLSearchParams |
Parsed query string |
useRouteMeta(context) |
Map<String, Object?> |
Merged route metadata |
useRouteState<T>(context) |
T? |
Typed navigation state |
useRouteURI(context) |
Uri |
Current route URI |
useFromLocation(context) |
HistoryLocation? |
Previous location |
API Reference
createRouter
Unrouter createRouter({
required Iterable<Inlet> routes,
Iterable<Guard>? guards,
String base = '/',
int maxRedirectDepth = 8,
History? history,
HistoryStrategy strategy = HistoryStrategy.browser,
})
createRouterConfig
RouterConfig<HistoryLocation> createRouterConfig(Unrouter router)
defineGuard
Guard defineGuard(Guard guard)
defineDataLoader
DataLoader<T> defineDataLoader<T>(
DataFetcher<T> fetcher, {
ValueGetter<T?>? defaults,
})