A simple but powerful path-based navigation router with full web-browser and deeplink support.

Overview

nav_stack

A simple but powerful path-based routing system, based on MaterialApp.router (Nav 2.0). It has browser / deeplink 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.1

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"]: StackRouteBuilder(builder: (_, __) => Container(color: Colors.purple)), //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 deep-link 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)

buildStackRoute() vs StackRouteBuilder?

Each entry in the PathStack requires a StackRouteBuilder() but to increase readability, we have added a .buildStackRoute() extension method on all Widgets. The only difference between the two, is that the full StackRouteBuilder allows you to inject args directly into your view using it's builder method.

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

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

Customizing MaterialApp

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 parser and delegate instances:

runApp(NavStack(
  appBuilder: (delegate, parser) => MaterialApp.router(
    routeInformationParser: parser,
    routerDelegate: delegate,
    debugShowCheckedModeBanner: false,
  ),
  stackBuilder: ...)

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

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 child routes use the scaffoldBuilder. For example, a classic 'Tab Style' app could look like:

runApp(NavStack(
  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: {
      ["/home"]: LoginScreen().buildStackRoute(),
      ["/profile"]: ProfileScreen().buildStackRoute(),
})));
...
class _TabScaffold extends StatelessWidget {
  ...
  Widget build(BuildContext context) {
    return Column(
      children: [
        // The current route
        Expanded(child: child),
        // A row of btns that call `NavStack.of(context).path = value` when pressed
        Row(children: [ Expanded(child: TextButton(...)), Expanded(child: TextButton(...)) ]),
      ]);}
}

Additionally, you can nest PathStack widgets to create sub-sections. Each with their own scaffold. For example here we wrap a nested-scaffold around all routes in the "/settings/" section of our app:

runApp(NavStack(
  stackBuilder: (context, controller) {
    return PathStack(
      scaffoldBuilder: (_, stack) => OuterTabScaffold(stack),
      routes: {
        ["/login", "/"]: LoginScreen().buildStackRoute(),
        // Nest a 2nd PathStack so all settings pages can share a secondary tab menu
        ["/settings/"]: PathStack(
          scaffoldBuilder: (_, stack) => InnerTabScaffold(stack),
          routes: {
            ["profile"]: ProfileScreen().buildStackRoute(),
            ["alerts"]: AlertsScreen().buildStackRoute(),
          },
        ).buildStackRoute(),
},);},));

Path Parsing Rules:

There are a number of rules that determine how paths are routed:

  • Routes with no trailing slash must an exact match:
    • eg, /details matches only /details not /details/, /details/12 or /details/?id=12
    • a special case is made for / which is always an exact match
  • Routes with a trailing slash, will accept a suffix,
    • eg, /details/ matches any of /details/, /details/12, /details/id=12&foo=99 etc
    • this allows endless levels of nesting and relative routes
  • If route has multiple paths, only the first one will be considered for the suffix check
    • eg, ["/details", "/details/"] requires exact match on either path
    • eg, ["/details/", "/details"] allows suffix on either path

Defining path and query-string arguments

Both path-based (/billing/88/99) or query-string (/billing/?foo=88&bar=99) args are supported.

In order to parse the args before they enter your view, you can use the StackRouteBuilder(). Consuming path-based args looks like:

["billing/:foo/:bar"]:
    StackRouteBuilder(builder: (_, args) => BillingPage(foo: args["foo"], bar: args["bar"])),

Consuming query-string args looks like:

["billing/"]:
    StackRouteBuilder(builder: (_, args) => BillingPage(id: "${args["foo"]}_${args["bar"]}")),

If you would like to access the args from within your view, and parse them there, you can just do:

NavStack.of(context).args;

For more information on how paths are parsed check out https://pub.dev/packages/path_to_regexp. To play with different routing schemes, you can use this demo: https://path-to-regexp.web.app/

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
  • navStack.history to access the history of path entries so far, you can modify and re-assign this list as needed
  • navStack.goBack() to go back one level in the history
  • navStack.popUntil(), navStack.popMatching(), navStack.replacePath() etc

Keeping it Old School

Importantly, you can still make full use of the old Navigator.push(), showDialog, showBottomSheet APIs, just be aware that none of these routes will be reflected in the navigation path. This can be quite handy for user-flows that do not necessarily need to be bound to browser history.

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

🕹 Advanced Usage

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

Regular Expressions

One powerful aspect of the path-base args is you can append Regular Expressions to the match.

  • eg, a route of /user/:foo(\d+) will match '/user/12' but not '/user/alice'
  • Don't worry if you don't know Regular Expressions, they are optional, and best used for advanced use cases

For more details on this parsing, check out the PathToRegExp docs: https://pub.dev/packages/path_to_regexp

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"] ?? "")

Route Guards

Guards allow you to intercept a navigation event on a per-route basis. Commonly used to prevent deep-links into unauthorized app sections.

To do this you can use the StackRouteBuilder.onBeforeEnter callback to run your own 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: (_, __) => guardAuthSection() )
...
bool guardAuthSection() {
  if (!appModel.isLoggedIn){
   // Schedule a redirect next frame
   NavStack.of(context).redirect("/login", () => showAuthWarningDialog(context));
   return false; // If we return false, the original route will not be entered.
  }
  return true;
}

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

Comments
  • My Impression of this library.

    My Impression of this library.

    I think this is an interesting approach. Just would like to leave some feedback.

    Navigator is a giant wrapper over Overlay which is a giant wrapper over Stack. I think you have reimplemented a bunch of things in here. This looks fine, but I wonder how you would support the hero animation since it is currently tightly bounded with the navigator route transition. Although this may not be a problem if we refactored hero controller. Ref ttps://github.com/flutter/flutter/issues/54200

    opened by chunhtai 1
  • Package status?

    Package status?

    Just wanted to know if this package (and flutter_path_stack) is still worked on, or discontinued.

    I'm on the look out for a good package that handles flutter router 2.0, with:

    • url parsing / deep-linking
    • web navigation
    • mobile back-button handling

    If you consider continuing this package pls let me know if there are any, issues/bugs , open/todo features, or any other help wanted. 🚀

    opened by SunlightBro 0
  • on 0.03

    on 0.03

    Missing NavStack class in source file of nav-stack it only has the controller not the class above it.

    Hence cannot use plugin as I have to soft fork include actual source and then it works.

    Must have been a Monday when plugin was released?

    No harm done, figured it out. I do like the direction that your Nav-2 approach is headed, peace.

    opened by fredgrott 0
  • example doesnt run - type 'Null' is not a subtype of type '_InheritedNavStackController'

    example doesnt run - type 'Null' is not a subtype of type '_InheritedNavStackController'

    state: PathStackState#c5533(ticker active)): type 'Null' is not a subtype of type '_InheritedNavStackController' in type cast

    The relevant error-causing widget was: PathStack .../flutter-nav-stack-master/example/lib/advanced_tabs_demo.dart:16:40

    opened by ezone-dev 0
  • A missing context in the demo causes the error

    A missing context in the demo causes the error

    Tried to open the demo, but got this

    ═══════════════════════╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY╞═══════════════════════
    The following _CastError was thrown building PathStack(dirty, dependencies:
    [_InheritedPathStack],
    state: PathStackState#e449a(ticker active)):
    type 'Null' is not a subtype of type '_InheritedNavStackController' in type cast
    
    The relevant error-causing widget was:
      PathStack
      ./flutter_nav_stack/example/lib/advanced_tabs_demo.dart:16:40
    
    

    fixed this stuff and wrapped in a SafeArea widget for better experience

    opened by mikekosulin 0
  • Cleaner hello world example

    Cleaner hello world example

    In hello_world_demo.dart, rather than creating a global NavStackController and assigning it in onGenerateStack, it may be better to wire up the keyboard navigation in scaffoldBuilder

    Example implementation:

    class HelloWorldDemo extends StatelessWidget {
      const HelloWorldDemo({
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp.router(
          routeInformationParser: NavStackParser(),
          routerDelegate: NavStackDelegate(
            onGenerateStack: (context, nav) => PathStack(
              scaffoldBuilder: (context, child) =>
                  KeyboardNavigationScaffold(child: child),
              routes: {
                ["/"]: HomeScreen().toStackRoute(),
                ["/messages"]: MessagesScreen().toStackRoute(),
                ["/profile"]: ProfileScreen().toStackRoute(),
              },
              transitionBuilder: (_, stack, anim1) =>
                  FadeTransition(opacity: anim1, child: stack),
            ),
          ),
        );
      }
    }
    
    class KeyboardNavigationScaffold extends StatefulWidget {
      const KeyboardNavigationScaffold({
        Key? key,
        this.child,
      }) : super(key: key);
    
      final Widget? child;
    
      @override
      _KeyboardNavigationScaffoldState createState() =>
          _KeyboardNavigationScaffoldState();
    }
    
    class _KeyboardNavigationScaffoldState
        extends State<KeyboardNavigationScaffold> {
      late final _controller = NavStack.of(context);
    
      @override
      void initState() {
        super.initState();
        RawKeyboard.instance.addListener((value) {
          if (value is RawKeyDownEvent) {
            if (value.logicalKey == LogicalKeyboardKey.digit1)
              _controller.path = "/login";
            if (value.logicalKey == LogicalKeyboardKey.digit2)
              _controller.path = "/profile";
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(child: widget.child);
      }
    }
    
    opened by chimon2000 0
Owner
gskinner team
We collaborate with smart, motivated clients to conceptualize, design, and build world-class interactive experiences.
gskinner team
A sample Google Chrome browser extension built in flutter web

My Activities It is a sample Google Chrome browser extension The extension was built in Flutter Web. How to build % cd /path/to/project/root % flutter

Viktor BORBÉLY 3 Nov 12, 2021
This is the semester 6 Mobile App Development Course project. So maybe the final project may not make sense ;) but it is a good place to start learning Flutter.

?? Overview MAD-Sem6 is a Mobile Development Course Project that contains Basic ➡️ Medium implementation of different widgets. As a whole it doesn't m

Muhammad Tayyab Asghar 3 Aug 9, 2021
Try to clone satria, with same feature but better ui

satria A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this i

Gesya Gayatree Solih 9 Nov 8, 2021
A full stack Instagram clone built with Flutter and Firebase

instagram_clone Responsive Instagram clone built with Flutter and Firebase. Getting Started This project is a starting point for a Flutter application

Simba Shi 2 Nov 26, 2022
🍔 A flutter app that shows food categories with various meals in each category and full description of each meal.

?? A flutter app that shows food categories with various meals in each category and full description of each meal.

Hemant Rajput 1 Jun 1, 2022
Add a powerful and customizable GIPHY picker into your Flutter app.

enough_giphy_flutter Add a powerful and customizable GIPHY picker into your Flutter app. Benefits Using enough_giphy_flutter has the following benefit

null 0 Apr 21, 2022
Beautiful Nike Web Design Concept With Flutter Beautiful Nike Web Design Concept With Flutter

flutter_web_nike A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started

Pawan Kumar 23 Jan 28, 2022
A Flutter application that allows users to post with full authentication

familicious_app A Flutter application that allows users to post with full authentication This project was built during a training session and improved

bayorwor 8 Oct 31, 2022
Full Stack TikTok Clone using Flutter, Firebase & GetX

TikTok Clone A Tiktok App- Works on Android & iOS! Features Authentication with Email & Password Uploading Videos with Caption Compressing Videos Gene

Rivaan Ranawat 200 Dec 30, 2022
Liquid swipe with Drawer - Liquid swipe application with full customizable drawer with govt logo

Liquid swipe with Drawer Liquid swipe application with full customizable drawer

Munem Sarker 1 Jan 19, 2022
Flutter-based mobile app displaying a list of daily curated content from top engineering blogs and articles. Backed by a GraphQL-based API written in Kotlin..

Flutter-based mobile app displaying a list of daily curated content from top engineering blogs and articles. Backed by a GraphQL-based API written in Kotlin..

Armel Soro 20 Dec 14, 2022
This app it's a simple app to help you choose between alcool or gasoline based on the price of the gasoline and the price of the alcool.

This app it's a simple app to help you choose between alcool or gasoline based on the price of the gasoline and the price of the alcool. It's a simple app that uses a simple logo, two text fields and a button that calculate and show the best option.

Cácio Lucas 0 Oct 3, 2021
A simple app for studying the japanese vocabulary you will learn in your japanese learning journey based on cards with meaning, pronunciation and kanji.

KanPractice A simple app for studying the japanese vocabulary you will learn in your japanese learning journey based on cards with meaning, pronunciat

Gabriel García 23 Jan 3, 2023
A package that allows you to add native drag and drop support into your flutter app.

native_drag_n_drop A package that allows you to add native drag and drop support into your flutter app. Currently supported features Support iPadOS 11

Alex Rabin 21 Dec 21, 2022
Phone-Store-App-UI-Flutter - Flutter Phone E-Store App UI with support for dark and light mode

Phone-Store-App-UI-Flutter - Flutter Phone E-Store App UI with support for dark and light mode

Jakub Sobański 2 Apr 30, 2022
Flutter Crypto App UI with support for dark and light mode

Beautiful Hotel Booking App UI with support for dark and light mode. Created by Martin Gogolowicz ( Martin Gogołowicz ).

Martin Gogołowicz 23 Dec 24, 2022
Currency Converter App with support for dark and light mode

Currency Converter App with support for dark and light mode. Created by Martin Gogolowicz ( Martin Gogołowicz ) | SobGOG

Martin Gogołowicz 10 Oct 31, 2022
A simple Whatsapp redesign dark mode concept based on UI Gradient Post on Instagram.

whatsapp_ui A simple Whatsapp redesign dark mode concept based on UI Gradient Post on Instagram.

António Pedro 13 Dec 16, 2022
Flutter wallpaper application, support IOS, Android

Wily WallPapers Mobile: IOS | Android Flutter The initial intention would be to create a wallpaper app with 30 images, and post it on the PlayStore wi

Luan Batista 7 Mar 22, 2022