Routemaster
Easy-to-use Navigator 2.0 router for web, mobile and desktop. URL-based routing, simple navigation of tabs and nested routes.
Install / Use
/learn @tomgilder/RoutemasterREADME
Routemaster <img src="https://openclipart.org/download/286938/Double-Decker-Bus.svg" width="30">
Hello! Routemaster is an easy-to-use router for Flutter, which wraps over Flutter's Router API.
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 250 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.
Documentation
Begin with the quick start below, but also see the API reference, wiki and FAQs.
<img src="https://openclipart.org/download/286938/Double-Decker-Bus.svg" width="80">
Quick start API tour
- Overview
- Routing
- Tabs
- Cupertino tabs
- Guarded routes
- 404 Page
- Redirect
- Swap routing map
- Navigation observers
- Navigate without a context
- Hero animations
Overview
Routemaster generates pages based on the current path. This is the key concept of its path-base routing. Path structure matters.
It uses the path to decide where a page should be pushed. This means the path needs to match your intended page hierarchy.
For example:
'/tabs': (route) => TabPage(child: HomePage(), paths: ['one', 'two']),
// First tab default page
'/tabs/one': (route) => MaterialPage(child: TabOnePage()),
// Second tab default page
'/tabs/two': (route) => MaterialPage(child: TabTwoPage()),
// Second tab sub-page: will be displayed in the 2nd tab because it
// starts with '/tabs/two'
'/tabs/two/subpage': (route) => MaterialPage(child: TabTwoPage()),
// Not a tab page: will not be displayed in in a tab
// because the path doesn't start with '/tabs/one' or '/tabs/two'
'/tabs/notInATab': (route) => MaterialPage(child: NotTabPage()),
Any child paths that begin with /tabs/one or /tabs/two will be pushed into the correct tab.
When navigating to /tabs/two/subpage, the TabPage will be asked "hey, do you know how to handle this path?" and it'll go "sure! it starts with /tabs/two, so it goes in my second tab".
However, navigating to /tabs/notInATab will not be displayed in a tab, but pushed on top of the tab bar.
TabPage will be all "yeah sorry, no idea what to do with that, doesn't match any of my tab paths" and its parent will be asked to hande it.
Path hierarchy matters, for example changing where dialogs are displayed.
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']),
),
'/products/myPage': (route) => MaterialPage(MyPage()),
})
Note that routes without path parameters have priority, so in the above example
/products/myPage will show MyPage.
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) PageStackNavigator(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()),
},
)
Passing path parameters from original to the redirect path:
RouteMap(routes: {
'/user/:id': (routeData) => MaterialPage(child: UserPage(id: id)),
'/profile/:uid': (routeData) => Redirect('/user/:uid'),
})
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 ext
Related Skills
node-connect
342.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
85.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
342.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
342.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
