routemaster 0.9.2 routemaster: ^0.9.2 copied to clipboard
Easy-to-use Navigator 2.0 router for web, mobile and desktop. URL-based routing, simple navigation of tabs and nested routes.
Routemaster #
Hello! Routemaster is an easy-to-use router for Flutter, which wraps over Navigator 2.0... and has a silly name.
Features #
- Simple declarative mapping from URLs to pages
- Easy-to-use API: just
Routemaster.of(context).push('/page')
- Really easy nested navigation support for tabs
- Multiple route maps: for example one for a logged in user, another for logged out
- Observers to easily listen to route changes
- Covered by over 160 unit, widget and integration tests
Here's the entire routing setup needed for an app featuring tabs and pushed routes:
final routes = RouteMap(
routes: {
'/': (_) => CupertinoTabPage(
child: HomePage(),
paths: ['/feed', '/settings'],
),
'/feed': (_) => MaterialPage(child: FeedPage()),
'/settings': (_) => MaterialPage(child: SettingsPage()),
'/feed/profile/:id': (info) => MaterialPage(
child: ProfilePage(id: info.pathParameters['id'])
),
}
);
void main() {
runApp(
MaterialApp.router(
routerDelegate: RoutemasterDelegate(routesBuilder: (context) => routes),
routeInformationParser: RoutemasterParser(),
),
);
}
And then to navigate:
Routemaster.of(context).push('/feed/profile/1');
...you can see this in action in this simple app example.
There's also a more advanced example.
I would love any feedback you have! Please create an issue for API feedback.
Migration from 0.7 to 0.8 #
-
StackNavigator
has been renamedPageStackNavigator
. -
Guard
properties have been renamed:validate
is nowcanNavigate
andonValidationFailed
is nowonNavigationFailed
. -
Note:
Guard
is no longer recommended. It's cleaner to use logic in the route map, like this:'/protected-route': (route) { if (!isLoggedIn()) return Redirect('/login'); if (!canUserAccessPage) return Redirect('/no-access'); return ProtectedPage(); }
Quick start API tour #
- Routing
- Tabs
- Cupertino tabs
- Guarded routes
- 404 Page
- Redirect
- Swap routing map
- Navigation observers
- Navigate without a context
Routing #
Basic app routing setup:
MaterialApp.router(
routerDelegate: RoutemasterDelegate(
routesBuilder: (context) => RouteMap(routes: {
'/': (routeData) => MaterialPage(child: PageOne()),
'/two': (routeData) => MaterialPage(child: PageTwo()),
}),
),
routeInformationParser: RoutemasterParser(),
)
Navigate from within pages:
Routemaster.of(context).push('relative-path');
Routemaster.of(context).push('/absolute-path');
Routemaster.of(context).replace('relative-path');
Routemaster.of(context).replace('/absolute-path');
Path parameters:
// Path '/products/123' will result in ProductPage(id: '123')
RouteMap(routes: {
'/products/:id': (route) => MaterialPage(
child: ProductPage(id: route.pathParameters['id']),
),
})
Query parameters:
// Path '/search?query=hello' results in SearchPage(query: 'hello')
RouteMap(routes: {
'/search': (route) => MaterialPage(
child: SearchPage(query: route.queryParameters['query']),
),
})
Get current path info within a widget:
RouteData.of(context).path; // Full path: '/product/123?query=param'
RouteData.of(context).pathParameters; // Map: {'id': '123'}
RouteData.of(context).queryParameters; // Map: {'query': 'param'}
Tabs #
Setup:
RouteMap(
routes: {
'/': (route) => TabPage(
child: HomePage(),
paths: ['/feed', '/settings'],
),
'/feed': (route) => MaterialPage(child: FeedPage()),
'/settings': (route) => MaterialPage(child: SettingsPage()),
},
)
Main page:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tabPage = TabPage.of(context);
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: tabPage.controller,
tabs: [
Tab(text: 'Feed'),
Tab(text: 'Settings'),
],
),
),
body: TabBarView(
controller: tabPage.controller,
children: [
for (final stack in tabPage.stacks) StackNavigator(stack: stack),
],
),
);
}
}
Cupertino tabs #
Setup:
RouteMap(
routes: {
'/': (route) => CupertinoTabPage(
child: HomePage(),
paths: ['/feed', '/settings'],
),
'/feed': (route) => MaterialPage(child: FeedPage()),
'/settings': (route) => MaterialPage(child: SettingsPage()),
},
)
Main page:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tabState = CupertinoTabPage.of(context);
return CupertinoTabScaffold(
controller: tabState.controller,
tabBuilder: tabState.tabBuilder,
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(
label: 'Feed',
icon: Icon(CupertinoIcons.list_bullet),
),
BottomNavigationBarItem(
label: 'Settings',
icon: Icon(CupertinoIcons.search),
),
],
),
);
}
}
Guarded routes #
Show default not found page if validation fails:
'/protected-route': (route) =>
canUserAccessPage()
? MaterialPage(child: ProtectedPage())
: NotFound()
Redirect to another page if validation fails (changes URL):
'/protected-route': (route) =>
canUserAccessPage()
? MaterialPage(child: ProtectedPage())
: Redirect('/no-access'),
Show another page if validation fails (doesn't change URL):
'/protected-route': (route) =>
canUserAccessPage()
? MaterialPage(child: ProtectedPage())
: MaterialPage(child: CustomNoAccessPage())
404 Page #
Default page to shown on unknown URL:
RouteMap(
onUnknownRoute: (route, context) {
return MaterialPage(child: NotFoundPage());
},
routes: {
'/': (_) => MaterialPage(child: HomePage()),
},
)
Redirect #
Redirect one route to another:
RouteMap(routes: {
'/one': (routeData) => MaterialPage(child: PageOne()),
'/two': (routeData) => Redirect('/one'),
})
Redirect all routes to login page, for a logged-out route map:
RouteMap(
onUnknownRoute: (_) => Redirect('/'),
routes: {
'/': (_) => MaterialPage(child: LoginPage()),
},
)
Swap routing map #
You can swap the entire routing map at runtime.
This is particularly useful for different pages depending on whether the user is logged in:
final loggedOutMap = RouteMap(
onUnknownRoute: (route, context) => Redirect('/'),
routes: {
'/': (_) => MaterialPage(child: LoginPage()),
},
);
final loggedInMap = RouteMap(
routes: {
// Regular app routes
},
);
MaterialApp.router(
routerDelegate: RoutemasterDelegate(
routesBuilder: (context) {
// This will rebuild when AppState changes
final appState = Provider.of<AppState>(context);
return appState.isLoggedIn ? loggedInMap : loggedOutMap;
},
),
routeInformationParser: RoutemasterParser(),
);
Navigation observers #
class MyObserver extends RoutemasterObserver {
// RoutemasterObserver extends NavigatorObserver and
// receives all nested Navigator events
@override
void didPop(Route route, Route? previousRoute) {
print('Popped a route');
}
// Routemaster-specific observer method
@override
void didChangeRoute(RouteData routeData, Page page) {
print('New route: ${routeData.path}');
}
}
MaterialApp.router(
routerDelegate: RoutemasterDelegate(
observers: [MyObserver()],
routesBuilder: (_) => routeMap,
),
routeInformationParser: RoutemasterParser(),
);
Navigate without a context #
app.dart
final routemaster = RoutemasterDelegate(
routesBuilder: (context) => routeMap,
);
MaterialApp.router(
routerDelegate: routemaster,
routeInformationParser: RoutemasterParser(),
)
my_widget.dart
import 'app.dart';
void onTap() {
routemaster.push('/blah');
}
Design goals #
- Integrated: work with the Flutter Navigator 2.0 API, don't try to replace it. Try to have a very Flutter-like API.
- Usable: design around user scenarios/stories, such as the ones in the Flutter storyboard - see here for examples.
- Opinionated: don't provide 10 options to achieve a goal, but be flexible for all scenarios.
- Focused: just navigation, nothing else. For example, no dependency injection.
This project builds on page_router.
Name #
Named after the original Routemaster:
(photo by Chris Sampson, licensed under CC BY 2.0)