routemaster 0.7.2 routemaster: ^0.7.2 copied to clipboard
Easy-to-use URL-based router for Flutter web, mobile and desktop that uses Navigator 2.0. Simple navigation of nested routes and tabs.
Routemaster #
Hello! Routemaster is an easy-to-use router for Flutter, which wraps over Navigator 2.0.
Features #
- Simple declarative mapping from a 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
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.6.0 to 0.7.0 #
-
The
path
property onRouteData
no longer returns the full path, including query string, to match Dart'sUri
object. The full including query string is now available from thefullPath
property. -
Builder
no longer takes achild
property, but abuilder
:Guard( validate: (info, context) => canShowPage(), builder: () => MaterialPage<void>(child: MyPage()), )
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: const 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 #
Redirect to another page if validation fails (changes URL):
'/protected-route': (route) => Guard(
validate: (route, context) => canUserAccessPage(),
onValidationFailed: (info, context) => Redirect('/no-access'),
child: MaterialPage(child: ProtectedPage()),
)
Show another page if validation fails (doesn't change URL):
'/protected-route': (route) => Guard(
validate: (route, context) => canUserAccessPage(),
onValidationFailed: (info, context) => NoAccessPage(),
child: MaterialPage(child: ProtectedPage()),
)
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'),
})
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: const 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: const RoutemasterParser(),
);
Navigate without a context #
app.dart
final routemaster = RoutemasterDelegate(
routesBuilder: (context) => routeMap,
);
MaterialApp.router(
routerDelegate: routemaster,
routeInformationParser: const 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)