path_stack 0.1.1+1 copy "path_stack: ^0.1.1+1" to clipboard
path_stack: ^0.1.1+1 copied to clipboard

outdated

A Stack that shows a single child route from a map of child routes. Similar to IndexedStack, but it uses Strings as keys and has a number of extra features like child nesting and path arguments.

nav_stack #

A simple but powerful path-based routing system, based on MaterialApp.router (Nav 2.0). It has browser / deep-link support and maintains a history stack as new routes are added.

Internally NavStack uses an IndexedStack to maintain a stateful list of routes which are defined declaratively and bound to the current MaterialApp.router path. It also provides a flexible imperative API for changing the path and modifying the history stack.

🔨 Installation #

dependencies:
  nav_stack: ^0.0.2

⚙ Import #

import 'package:nav_stack/nav_stack.dart';

🕹ī¸ Basic Usage #

Hello NavStack #

NavStack wraps MaterialApp, so you can include it as the root-element in your App:

runApp(
  NavStack(stackBuilder: (_, controller){
        // Path stack does all the heavy lifting when it comes to arranging our routes
        // Read more here: https://pub.dev/packages/path_stack#defining-paths
      return PathStack(
        routes: {
          ["/page1"]: Container(color: Colors.red).buildStackRoute(),
          ["/page2"]: Container(color: Colors.green).buildStackRoute(),
          // Nesting allows you to type relative paths, and to also wrap sub-sections in their own menus/scaffold
          ["/page3/"]: PathStack(
            routes: {
               // Paths can have multiple entries, allowing aliases,
               // Using "" alias here allows this route to match "page3/" or "page3/subPage1"
              ["subPage1", ""]: Container(color: Colors.orange).buildStackRoute(),
              ["subPage2"]: Container(color: Colors.purple).buildStackRoute(), //matches: /page3/subPage2
            },
          ).buildStackRoute()});
}
}));
...
// Change path using a simple api:
void showPage1() => NavStack.of(context).path = "/page1";
void showSubPage2() => NavStack.of(context).path = "/page3/subPage2";

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 controller which 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)

Nesting

One of the key features of this package is that it has top-level support for wrapping child routes in a shared widget (aka 'nesting'). To supply a custom Scaffold around all routes in a Stack, just use the scaffoldBuilder.

For example, a classic 'tab-menu' app could look like:

runApp(NavStack(
  // The controller provides us read/write control over the current path
  stackBuilder: (context, controller) => PathStack(
    // Use scaffold builder to wrap all our pages in a stateful tab-menu
    scaffoldBuilder: (_, stack) => _TabScaffold(["/home", "/profile"], child: stack),
    routes: {
      // Declare your routing table, these routes can nest other `PathStack` components
      ["/home"]: LoginScreen().buildStackRoute(),
      ["/profile"]: ProfileScreen().buildStackRoute(),
})));

Additionally, you can nest multiple PathStacks within eachother to create sub-sections. Each with their own scaffold. For example:

runApp(NavStack(
  stackBuilder: (context, controller) {
    return PathStack(
      scaffoldBuilder: (_, stack) => OuterTabScaffold(stack),
      routes: {
        ["/login", "/"]: LoginScreen().buildStackRoute(),
        ["/settings/"]: PathStack(
          scaffoldBuilder: (_, stack) => InnerTabScaffold(stack),
          routes: {
            ["profile"]: ProfileScreen().buildStackRoute(),
            ["alerts"]: AlertsScreen().buildStackRoute(),
          },
        ).buildStackRoute(),
},);},));

Path Parsing Rules:

  • Paths with no trailing slash must an exact match:
    • eg, /details matches only /details not /details/12 or /details/?id=12
  • Paths with a trailing slash, will accept a suffix,
    • eg, /details/ matches any of /details/, /details/12, /details/id=12&foo=99 etc
  • If an entry has multiple paths, only the first one will be considered for the suffix check
    • eg, ["/details", "/"] matches only /details or /

For more info on constructing nested paths, check out the PathStack docs: https://pub.dev/packages/path_stack#defining-paths

buildStackRoute() vs StackRouteBuilder

Each entry in the PathStack requires a StackRouteBuilder(). To increase readability, we have added a .buildStackRoute() extension method on all Widgets. These calls are functionally identical, except that StackRouteBuilder allows you to inject args directly into your view.

When your view does not require args, then the extensions tend to be more readable:

["/login"]: LoginScreen().buildStackRoute(),
VS
["/login"]: StackRouteBuilder(builder: (_, __) => LoginScreen()),

Imperative API #

NavStack offers a strong imperative API for interacting with your navigation state.

  • NavStackController can be looked up at anytime with NavStack.of(context)
  • navStack.path to change the global routing path
  • [TODO] navStack.history to access the history of path entries so far, you can modify and re-assign this list as needed
  • [TODO] navStack.goBack() to go back one level in the history
  • [TODO] navStack.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 routes will be reflected in the navigation path.

Important: The entire NavStack exists within a single PageRoute. This means that calls to Navigator.of(context).pop() from within the NavStack children will be ignored. However, you can still use .pop() them from within Dialogs, BottomSheets or full-screen PageRoutes triggered with Navigator.push().

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:

runApp(NavStack(
  appBuilder: (delegate, router) => MaterialApp.router(
    routeInformationParser: router,
    routerDelegate: delegate,
    debugShowCheckedModeBanner: false,
  ),
  entries: { ... })

Note: Do not wrap a second MaterialApp around NavStack or you will break all browser support and deeplinking.

🕹 Advanced Usage #

In addition to basic nesting and routing, NavStack supports advanced features including Aliases, Route Guards and Param Args.

Aliases

Each route entry can have multiple paths allowing it to match any of them. For example, we can setup a route to match both /home and /:

["/home", "/"]: LoginScreen().buildStackRoute(),

Or a route that accepts optional named params:

["/messages/", "/messages/:messageId"]: // matches both "/messages/" and "messages/99"
    StackRouteBuilder(builder: (_, args) => MessageView(args["messageId"] ?? "")

Defining paths and arguments #

Both path-based or query-string args are supported by the include PathStack components.

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:

Route Guards

Before a route is loaded by PathStack, you can use the onBeforeEnter callback to run custom logic, and decide whether to block the change.

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

Since guards are just functions, you can easily re-use them across routes, and they can also be applied to entire sections by nesting a PathStack component.

Putting it Together

Here's a a more complete example showing nested stacks, and an entire section that requires the user to be logged in. Otherwise they are redirected to /login:

bool isLoggedIn = false;

return NavStack(
  stackBuilder: (context, controller) {
    return PathStack(
      scaffoldBuilder: (_, stack) => _MyScaffold(stack),
      routes: {
        ["/login", "/"]: LoginScreen().buildStackRoute(),
        ["/in/"]: PathStack(
          routes: {
            ["profile/:profileId"]:
                StackRouteBuilder(builder: (_, args) => ProfileScreen(profileId: args["profileId"] ?? "")),
            ["settings"]: SettingsScreen().buildStackRoute(),
          },
        ).buildStackRoute(onBeforeEnter: (_) {
          if (!isLoggedIn) controller.redirect("/login", () => showAuthWarning(context));
          return isLoggedIn; // If we return false, the route will not be entered.
        }),
      },
    );
  },
);
...
void handleLoginPressed() => NavStack.of(context).path = "/login";
void showProfile() => NavStack.of(context).path = "/in/profile/23"; // Blocked
void showSettings() => NavStack.of(context).path = "/in/settings"; // Blocked

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"

There are many other options you can provide to the PathStack, including unknownPathBuilder, transitionBuilder and, basePath. For an exhaustive list, check out this example:

🐞 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

6
likes
0
pub points
3%
popularity

Publisher

unverified uploader

A Stack that shows a single child route from a map of child routes. Similar to IndexedStack, but it uses Strings as keys and has a number of extra features like child nesting and path arguments.

Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter, path_to_regexp

More

Packages that depend on path_stack