routemaster 1.0.1 copy "routemaster: ^1.0.1" to clipboard
routemaster: ^1.0.1 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.

Build codecov pub

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(),
      ),
  );
}
copied to clipboard

And then to navigate:

Routemaster.of(context).push('/feed/profile/1');
copied to clipboard

...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.


Quick start API tour #

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()),
copied to clipboard

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(),
)
copied to clipboard
Routemaster.of(context).push('relative-path');
Routemaster.of(context).push('/absolute-path');

Routemaster.of(context).replace('relative-path');
Routemaster.of(context).replace('/absolute-path');
copied to clipboard

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()),
})
copied to clipboard

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']),
      ),
})
copied to clipboard

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'}
copied to clipboard

Tabs #

Setup:

RouteMap(
  routes: {
    '/': (route) => TabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)
copied to clipboard

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),
        ],
      ),
    );
  }
}
copied to clipboard

Cupertino tabs #

Setup:

RouteMap(
  routes: {
    '/': (route) => CupertinoTabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)
copied to clipboard

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),
          ),
        ],
      ),
    );
  }
}
copied to clipboard

Guarded routes #

Show default not found page if validation fails:

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : NotFound()
copied to clipboard

Redirect to another page if validation fails (changes URL):

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : Redirect('/no-access'),
copied to clipboard

Show another page if validation fails (doesn't change URL):

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : MaterialPage(child: CustomNoAccessPage())
copied to clipboard

404 Page #

Default page to shown on unknown URL:

RouteMap(
    onUnknownRoute: (route, context) {
        return MaterialPage(child: NotFoundPage());
    },
    routes: {
        '/': (_) => MaterialPage(child: HomePage()),
    },
)
copied to clipboard

Redirect #

Redirect one route to another:

RouteMap(routes: {
    '/one': (routeData) => MaterialPage(child: PageOne()),
    '/two': (routeData) => Redirect('/one'),
})
copied to clipboard

Redirect all routes to login page, for a logged-out route map:

RouteMap(
  onUnknownRoute: (_) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
)
copied to clipboard

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'),
})
copied to clipboard

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(),
);
copied to clipboard
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(),
);
copied to clipboard

app.dart

final routemaster = RoutemasterDelegate(
  routesBuilder: (context) => routeMap,
);

MaterialApp.router(
  routerDelegate: routemaster,
  routeInformationParser: RoutemasterParser(),
)
copied to clipboard

my_widget.dart

import 'app.dart';

void onTap() {
  routemaster.push('/blah');
}
copied to clipboard

Hero animations #

Hero animations will work automatically on the top-level navigator (assuming you're using MaterialApp or CupertinoApp).

For any child navigators, you'll need to wrap PageStackNavigator in a HeroControllerScope, like this:

HeroControllerScope(
  controller: MaterialApp.createMaterialHeroController(),
  child: PageStackNavigator(
    stack: pageStack,
  )
)
copied to clipboard

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:

A photo of a Routemaster bus

(photo by Chris Sampson, licensed under CC BY 2.0)

681
likes
130
points
9.62k
downloads

Publisher

verified publishertom.gilder.dev

Weekly Downloads

2024.09.29 - 2025.04.13

Easy-to-use Navigator 2.0 router for web, mobile and desktop. URL-based routing, simple navigation of tabs and nested routes.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, flutter_web_plugins, path

More

Packages that depend on routemaster