nav_stack 0.0.1+2
nav_stack: ^0.0.1+2 copied to clipboard
A simple but powerful path-based routing system. Based on MaterialApp.router (Nav 2.0) it has full web-browser and deeplink support.
nav_stack #
A simple but powerful path-based routing system. Based on MaterialApp.router (Nav 2.0) it has full web-browser and deeplink support.
NavStack maintains a stateful list of routes which are defined declaratively and bound to MaterialApp.router. It also provides a flexible imperative API for changing the path and modifying the history stack.
đ¨ Installation #
dependencies:
nav_stack: ^0.0.1
â Import #
import 'package:nav_stack/nav_stack.dart';
đšī¸ Usage #
When it comes to declaring your routes structure, NavStack uses PathStack under the hood. There is a wide variety of routing configurations you can create. We'll walk through some basic uses cases here, but check out the PathStack docs for more info https://pub.dev/packages/path_stack.
Hello NavStack #
Your most basic tab-based app might look like this:
return NavStack(
stackBuilder: (context, controller) => PathStack(
path: controller.path,
// Use scaffold builder to wrap all our pages in a tab-menu
scaffoldBuilder: (_, stack) => _TabScaffold(["/home", "/profile"], child: stack),
routes: {
["/home"]: LoginScreen().buildStackRoute(),
["/profile"]: ProfileScreen().buildStackRoute(),
}));
...
void handleHomeTabPressed() => NavStack.of(context).path = "/home";
void handleProfileTabPressed() => NavStack.of(context).path = "/profile";
This might not look like much, but there is a lot going on here.
- This is fully bound to the browser path,
- It will also receive deeplink start up values on any platform,
- It provides a
controllerwhich you can use to easily change the global path at any time, - All routes are persistent, maintaining their state as you navigate between them (optional)
- A persistent
_TabScaffoldis wrapping all routes, and it is also stateful
Note: String literals ("/home") are used here for brevity and clarity. In real usage, it is recommended you give each page it's own path property like HomePage.path or LoginScreen.path. This makes it much easier to construct and share links from other sections in your app: controller.path = "${SettingsPage.path}{ProfilePage.path}$profileId"
Aliases
Each route entry can have multiple paths, which act as aliases. For example, we can setup a route to match both /home and /:
["/home", "/"]: LoginScreen().buildStackRoute(),
Route Guards
Before a route is loaded, you can check whether the user is authorized, and re-direct using onBeforeEnter callback. Since guards are just functions, you can easily re-use them across routes.
For example, this guard will redirect to LoginScreen and show a warning dialog (but you can do whatever you want):
// You can use either the `buildStackRoute` or `StackRouteBuilder` to add guards
["/admin"]: AdminPanel().buildStackRoute(onBeforeEnter: guardAuthSection),
["/admin"]: StackRouteBuilder(builder: (_, __) => AdminPanel(), onBeforeEnter: ... )
...
bool guardProtectedSection(String newPath, NavStackController controller) {
if (!appModel.isLoggedIn) controller.redirect("/login", () => showAuthWarningDialog(context));
return appModel.isLoggedIn; // If we return false, the route will not be entered.
}
Putting it Together
Here's a a more complete example with an entire section that requires the user to be logged in. Otherwise they are redirected to /home:
return NavStack(
stackBuilder: (context, controller) {
return PathStack(
path: controller.path,
scaffoldBuilder: (_, stack) => _MyScaffold(stack),
routes: {
["/login", "/"]: LoginScreen().buildStackRoute(),
["/in/"]: PathStack(
path: controller.path,
basePath: "/in/",
routes: {
["profile/:profileId"]:
StackRouteBuilder(builder: (_, args) => ProfileScreen(profileId: args["profileId"] ?? "")),
["settings"]: SettingsScreen().buildStackRoute(),
},
).buildStackRoute(onBeforeEnter: (_) {
if (!isConnected) controller.redirect("/login", () => showAuthWarning(context));
return isConnected; // If we return false, the route will not be entered.
}),
},
);
},
);
There are many other options you can provide to the PathStack, including unknownPathBuilder, transitionBuilder and, basePath. For an exhaustive list, check out this example:
Defining paths and arguments #
Both path-based or query-string args are supported by PathStack under the hood. For more information on the routing rules and options check out the docs in the PathStack package: https://pub.dev/packages/path_stack#defining-paths
As a quick refresher, consuming path-based args (/billing/88/99) looks like:
["billing/:foo/:bar"]:
StackRouteBuilder(builder: (_, args) => BillingPage(id: "${args["foo"]}_${args["bar"]}")),
Consuming query-string args (/billing/?foo=88&bar=99) looks like:
["billing/"]:
StackRouteBuilder(builder: (_, args) => BillingPage(id: "${args["foo"]}_${args["bar"]}")),
Note: The buildStackRoute extension method will not work in this case, as there is no opportunity to provide the child widget with arguments. You must use StackRouteBuilder if you want to parse args and inject them into your views.
For a some more complex code examples of path structure you can check here:
- https://github.com/gskinnerTeam/flutter_path_stack/blob/master/example/lib/simple_tab_example.dart
- https://github.com/gskinnerTeam/flutter_path_stack/blob/master/example/lib/advanced_tab_example.dart
Imperitive API #
NavStack offers a strong imperitive API for interacting with your navigation state.
NavStackControllercan be looked up at anytime withNavStack.of(context)navStack.pathto change the global routing pathnavStack.historyto access the history of path entries so far, you can modify and re-assign this list as needednavStack.goBack()to go back one level in the historynavStack.popUntil(),navStack.popMatching(),navStack.replacePath()etc
Additionally, you can still make full use of the old Navigator.push() API, and showDialog, showBottomSheet etc, just be aware that none of these things will be reflected in the navigation path. For example, you can not deeplink directly to a dialog or a bottom sheet.
Important: Any calls to Navigator.of(context).pop() from within the NavStack children will be ignored. However, you can still use them from within Dialogs, BottomSheets or full-screen PageRoutes triggered with Navigator.push(). If you'd like to pop() something that is a descendant of NavStack just use NavStack.of(context).goBack().
MaterialApp.router() #
NavStack creates a default MaterialApp.router internally, but you can provide a custom one if you need to modify the settings. Just use the appBuilder and pass along the provided router and delegate instances:
return NavStack(
appBuilder: (router, delegate) => MaterialApp.router(
routeInformationParser: delegate,
routerDelegate: router,
debugShowCheckedModeBanner: false,
),
entries: { ... }
Note: Do not wrap a second MaterialApp around NavStack or you will break all browser support and deeplinking.
đ Bugs/Requests #
If you encounter any problems please open an issue. If you feel the library is missing a feature, please raise a ticket on Github and we'll look into it. Pull request are welcome.
đ License #
MIT License