path_stack

A Stack that shows a single child from a list of children. Similar in concept to IndexedStack, but it uses Strings as keys and has a number of extra features like child nesting, custom animations and path arguments.

🔨 Installation

dependencies:
  path_stack: ^0.0.2

⚙ Import

import 'package:path_stack/path_stack.dart';

🕹ī¸ Usage

In it's simplest form, it is just a list of route names, and their associated Widgets:

    return PathStack(path: path, routes: {
      ["page1"]: Container(color: Colors.red).toStackRoute(),
      ["page2"]: Container(color: Colors.green).toStackRoute(),
      // Nesting allows you to type relative paths, and to also wrap sub-sections in their own menus/scaffold
      ["page3/"]: PathStack(
        path: path,
        basePath: "page3/",
        routes: {
           // Paths can have multiple entries, allowing aliases, here "" allows this route to match "page3/"
          ["subPage1", ""]: Container(color: Colors.orange).toStackRoute(),
          ["subPage2"]: Container(color: Colors.purple).toStackRoute(), //matches: /page3/subPage2
        },
      ).toStackRoute(),
    });
  }

If you're wondering .toStackRoute() is an extension that converts any widget into a StackRouteBuilder. This is just to make the tree more readable. You can use this in cases where the view does not need to parse any path params. If you do need to parse params, you can use the full builder:

["subPage2/"]: StackRouteBuilder(builder: (_, args) => Container(child: Text(args["id"]))),
// This would show "99" for a path of: "/page3/?id=99"

Maintain State

Like IndexedStack, one of the core features of PathStack is that all child routes are kept-alive. When building routes you can pass a maintainState value, which will tell the stack whether you'd like the route to keep it's state or not. By default this is set to true.

What this means in practice, is that children in the stack will maintain their state, scroll positions, text fields, search filters, ordering, animation positions and any other state that you have! If you'd like a route to not maintainState, just set this value to false.

Scaffold Builder

To support the common use case of shared navigation or app chrome, there is scaffoldBuilder which lets you wrap any Widget around the stack. This means you can create a basic tab-scaffold like this:

class _SimpleTabExampleState extends State<SimpleTabExample> {
  PageType _currentPage = PageType.Home;
  @override
  Widget build(BuildContext context) {
    String currentPath = describeEnum(_currentPage);
    return PathStack(
      path: currentPath,
      // Optionally you can define a custom animation when paths are changed
      transitionBuilder: (_, stack, anim) => FadeTransition(opacity: anim),
      // TabScaffold can be anything you want. Here it's just a column w/ 3 btns, and dispatches a pressed event
      scaffoldBuilder: (_, stack) => TabScaffold(_currentPage, child: stack, onTabPressed: _handleTabPressed),
      routes: {
        ["${describeEnum(PageType.Home)}"]: HomePage().toStackRoute(),
        ["${describeEnum(PageType.Settings)}"]: SettingsPage().toStackRoute(),
        ["${describeEnum(PageType.Explore)}"]: ExplorePage().toStackRoute(maintainState: false),
      },
    );
  }
  // Change page type and rebuild the parent view
  void _handleTabPressed(PageType value) => setState(() => _currentPage = value);
}

In the example above you can see some of the other core API's:

  • PathStack.transitionBuilder - Allows you to provide a custom animation when path is changed
  • PathStack.routes - A list of available paths for this stack, can contain other PathStacks to nest paths and scaffolds
  • buldStackRoute(bool maintainState) - Set this to false if you do not want a path to remember it's State.

Defining paths

path_stack supports both path based args (/details/83) or queryString args (/details?id=83). Under the hood we use path_to_reg_exp so you can look there for details on path parsing.

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
Path Params
  • To accept path params you must name them /details/:foo/:bar, which will match details/10/20
  • This can be accessed later in the PathStack.builder using args["foo"] and args["bar"]
Query Params
  • To accept a query parameter you can use a trailing slash /details/ which will match any appended value
  • This can be accessed later in the PathStack.builder using args["paramName"]

Full Api Example

path_stack supports easy nesting of stacks using the PathStack.parentPath property combined with the PathStackEntry.builder.

For a full API tour, you can view the following code sample:

PathStack(
  // Path is the source of truth for each stack, usually this is shared by all child stacks
  path: currentPath,
  // Optional: Provide custom widget for unknown paths
  unknownPathBuilder: (_) => Center(child: Text("Custom 404 Page")),
  // Optional: Provide custom animationIn (default is no animation)
  transitionBuilder: (_, stack, anim1) => FadeTransition(opacity: anim1, child: stack),
  // Optional: Provide duration for animationIn
  transitionDuration: Duration(milliseconds: 200),
  // By default pages are case insensitive, like a web server. But you can turn this off
  caseSensitive: true,
  // Define all matching routes for this stack
  routes: {
    ["/home"]: HomePage().toStackRoute(),
    // Adding a "/" at the end of any path indicates it can match as a prefix
    ["/settings/"]: PathStack(
      path: currentPath,
      // Set a basePath that will be combined with all routes in this child stack
      basePath: "/settings/",
      // Use scaffoldBuilder to add a shared scaffold to all of this stacks child routes
      scaffoldBuilder: (_, stack) => Column(children: [
        Padding(padding: EdgeInsets.all(20), child: Text("Shared Settings App Bar")), // Settings Header
        Expanded(child: stack),
      ]),
      // Define child routes for settings section
      routes: {
        // By adding a "" alias, the first route will match both `/settings/alerts` and `/settings/`
        ["alerts", ""]: AlertsPage().toStackRoute(),
        ["profile"]: ProfilePage().toStackRoute(),
        // When you need the args use the full builder. You can use path format, or query string format to pass args.
        // Path Format needs to define how many arguments it wants, and their names.
        ["billing_alt/:foo/:bar"]:
            StackRouteBuilder(builder: (BuildContext context, Map<String, String> args) {
          return BillingPage(id: "${args["foo"]}_${args["bar"]}");
        }),
        // Query string format does not need to define arg names up front.
        // This will catch billing/ + queryParams, and by adding a "billing" alias, we'll also catch links without the trailing slash
        ["billing/", "billing"]: StackRouteBuilder(builder: (BuildContext context, Map<String, String> args) {
          return BillingPage(id: "${args["foo"]}_${args["bar"]}");
        }),
      },
    ).toStackRoute(),
  },
)

Complex Nesting Example

For an example that shows complex nesting scenarios, including multiple scaffolds, full screen routes check out the advanced tab example: https://github.com/gskinnerTeam/flutter_path_stack/blob/master/example/lib/advanced_tab_example.dart

🐞 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

Libraries

path_stack