Declarative, type-safe, codegen-free router for flutter.
Other popular routing solutions use url strings to represent their state. That leads to problems like having to serialize values to pass them between screens, or not being able to save the state of nested routes. HYPER_ROUTER stores its state as a datastructure instead, which allows much more flexibility and makes the API more coincise.
HYPER_ROUTER still supports web and deep-linking, which require serializing objects, but it makes it optional, so you only have to implement it when you actually need it.
- Value-based navigation
- Declarative
- Route guards
- Nested navigation with state preservation
- Returning a value from a route
- Optional URL support
- Highly extensible
- No code-gen
- Declare your route tree. Each node in the tree is associated with a unique key.
- Access the controller: Use
HyperRouter.of(context)
orcontext.hyper
to interact with the router. - Navigate: Push new routes onto the stack using their associated keys with
context.hyper.navigate(<key>)
. - Pop routes: Return to the previous screen using
context.hyper.pop
orNavigator.of(context).pop
.
final router = HyperRouter(
initialRoute: HomeScreen.routeName,
routes: [
ShellRoute(
shellBuilder: (context, controller, child) =>
MainTabsShell(controller: controller, child: child),
tabs: [
NamedRoute(
screenBuilder: (context) => const HomeScreen(),
name: HomeScreen.routeName,
children: [
NamedRoute(
screenBuilder: (context) => const ProductListScreen(),
name: ProductListScreen.routeName,
children: [
ValueRoute<ProductRouteValue>(
screenBuilder: (context, value) =>
ProductDetailsScreen(value: value),
),
],
),
],
),
NamedRoute(
screenBuilder: (context) => const GuideScreen(),
name: GuideScreen.routeName,
),
NamedRoute(
screenBuilder: (context) => const SettingsScreen(),
name: SettingsScreen.routeName,
),
],
),
],
);
Notice the 3 types of routes:
NamedRoute
: A basic route, associated with a unique name.ValueRoute<T>
: A route that lets you to pass data to another screen.ShellRoute
: A route that wraps a nested navigator with an interface surrounding it, such as a tab bar.
The keys, associated with routes, are hidden in RouteValue
instances:
RouteName
(forNamedRoute
) uses the provided string as its key.- Your custom type
T
forValueRoute<T>
usesT
as the key.
Use for simple navigation between screens that doesn't require passing data.
- Declare the route name:
class HomeScreen extends StatelessWidget {
static const routeName = RouteName('home');
// ...
}
- Navigate:
HyperRouter.of(context).navigate(HomeScreen.routeName);
// Or, for convenience:
// context.hyper.navigate(HomeScreen.routeName);
Use a ValueRoute<T>
if you need to pass data to the route during navigation.
- Declare the value type by extending
RouteValue
:
class ProductRouteValue extends RouteValue {
const ProductRouteValue(this.product);
final Product product;
}
- To navigate to the route, pass a value of your type to the navigator:
context.hyper.navigate(ProductRouteValue(
Product(/*...*/)
));
Use ShellRoute
to create a bottom navigation bar.
Arguments:
shellBuilder
: the screen that wraps the child route and displays the tab bar.tabs
: the routes that will be displayed inside the shell.
The shell builder is provided with a ShellController
.
Using the ShellController
:
setTabIndex(index)
: Switch to the tab at the specified index.tabIndex
: Get the index of the currently active tab.
Btw: Internally,
ShellRoute
, likeValueRoute
, also has aRouteValue
associated with it that contains the state of each tab.
Example:
import 'package:flutter/material.dart';
import 'package:hyper_router/hyper_router.dart';
class TabsShell extends StatelessWidget {
const TabsShell({
required this.controller,
required this.child,
super.key,
});
final Widget child;
final ShellController controller;
@override
Widget build(BuildContext context) {
final i = controller.tabIndex;
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
onDestinationSelected: (value) {
controller.setTabIndex(value);
},
selectedIndex: controller.tabIndex,
destinations: [
NavigationDestination(
icon: Icon(i == 0 ? Icons.home_outlined : Icons.home),
label: "Home",
),
NavigationDestination(
icon: Icon(i == 1 ? Icons.shopping_bag_outlined : Icons.shopping_bag),
label: "Cart",
),
NavigationDestination(
icon: Icon(i == 2 ? Icons.settings_outlined : Icons.settings),
label: "Settings",
),
],
),
);
}
}
Receiving the result:
final result = await context.hyper.navigate(FormScreen.routeName);
Returning the result:
// FormScreen
context.hyper.pop(value);
Native flutter push & pop work too. For example, showing a dialog:
final result = await showDialog(Dialog(...));
Navigator.of(context).pop(value);
Use the riderect callback to control navigation flow based on conditions like authentication status:
final router = HyperRouter(
redirect: (context, state) {
final authCubit = context.read<AuthCubit>();
// Check if user is logged in and trying to access an authenticated route
if (!authCubit.state.authenticated &&
state.stack.containsNode(AuthwalledScreen.routeName.key)) {
return AuthScreen.routeName; // Redirect to authentication
}
return null; // No redirection needed
},
// ...
);
state.stack
Represents the upcoming navigation stack. The first element will be at the bottom, the last at the top.stack.containsNode
Checks if a route with the provided key exists in the stack. Notice that it requires providing thekey
explicitly.- Return:
- The route key to redirect the user. This is the same value you would use for
navigate
. null
, if no redirect is necessary.
- The route key to redirect the user. This is the same value you would use for
There are two use-cases that require URL support: web apps and deep linking. Since most Flutter apps are targetting mobile platforms, and deep linking usually covers only a few destinations, HYPER_ROUTER was designed to make URL support optional.
By default, your app will work in the browser just fine, but the URL will not be updating. To fix that, set the enableUrl
property to true
:
final router = HyperRouter(
enableUrl: true,
// ...
);
A segment is a part of the URL separated by a slash (
/
).
Now, you need to make sure that every route can be parsed to and from a URL segment. NamedRoute
supports parsing by default, but ValueRoute
needs to be provided with a parser.
Creating a URL parser:
Here we're creating a parser for ProductRouteValue
. We want the url to look like this: home/products/<productID>
. The parser is responsible for the <productId>
segment:
class ProductSegmentParser extends UrlSegmentParser<ProductRouteValue> {
@override
ProductRouteValue? decodeSegment(SegmentData segment) {
return ProductRouteValue(segment.name);
}
@override
SegmentData encodeSegment(ProductRouteValue value) {
return SegmentData(name: value.productId);
}
}
You can optionally provide query parameters to SegmentData
(queryParams
field). They will be placed at the end of the URL. If the stack contains more than one route with query parameters, they'll be combined.
decodeSegment
should return null
if it doesn't recognize the segment.
segment.state
is stored in the browser's history. You can put the data that you don't want visible in the URL there, and it will be restored when the user navigates using browser's back and forward buttons.
TODO (although probably already possible)
I tried to design the package to be highly extensible to make it possible to create a route for any werid and unusual use-case. As an example, in the demo app, I created a responsive list-detail view: it displays the list and the detail pages side by side on a wide screen (similarly to a shell route), and regularly, on top of each other, on a small screen.
How the router works on the inside:
- The route tree is traversed from the target route to the root to construct a linked list of
RouteNode
s. - Each node is responsible for building its own page and - recursively - all the consecutive pages. This happens inside the
createPages
method that returns the list of pages.NamedRoute
andValueRoute
just place their own and all the consecutive pages on top of each other:Iterable<Page> createPages(BuildContext context) { final page = buildPage(context); return [page].followedByOptional(next?.createPages(context)); }
ShellRoute
only places its own page into the list, while all its children go into the nested navigator inside the shell page.
- To create your own route, you need to override these two classes:
HyperRoute
andRouteNode
.
This should be enough to understand the example from the demo.