unrouter 0.11.0
unrouter: ^0.11.0 copied to clipboard
Declarative, guard-driven router for Flutter with nested routes and typed helpers.
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,
})