nav_stack

一个简单但强大的基于路径的路由系统,基于MaterialApp.router(Nav 2.0)。它支持浏览器/深层链接,并在添加新路由时维护历史堆栈。

NavStack 内部使用 IndexedStack 来维护一个有状态的路由列表,这些路由是声明式定义的,并绑定到当前的 MaterialApp.router 路径。它还提供了一个灵活的命令式 API,用于更改路径和修改历史堆栈。

? 安装

dependencies:
  nav_stack: ^0.0.1

⚙ 导入

import 'package:nav_stack/nav_stack.dart';

?️ 基本用法

你好 NavStack

NavStack 包装了 MaterialApp,所以你可以将其作为你应用程序的根元素包含进来。

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";

这看起来可能没什么大不了,但这里面有很多东西在起作用。

  • 这完全绑定到浏览器路径,
  • 它还将在任何平台上接收深层链接启动值,
  • 它提供了一个 controller,你可以使用它随时轻松地更改全局路径,
  • 所有路由都是持久的,在它们之间导航时保持其状态(可选)。

buildStackRoute() 与 StackRouteBuilder?

PathStack 中的每个条目都需要一个 StackRouteBuilder(),但为了提高可读性,我们在所有 Widget 上添加了一个 .buildStackRoute() 扩展方法。两者唯一的区别是,完整的 StackRouteBuilder 允许你通过其 builder 方法将参数直接注入你的视图。

当你的视图不需要参数时,扩展方法会更具可读性。

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

自定义 MaterialApp

NavStack 会在内部创建一个默认的 MaterialApp.router,但如果你需要修改设置,可以提供一个自定义的。只需使用 appBuilder 并传递提供的 parserdelegate 实例即可。

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

注意: 不要将第二个 MaterialApp 包装在 NavStack 外面,否则会破坏所有浏览器支持和深层链接。

嵌套

此包的一个关键特性是它具有对将子路由包装在共享 Widget(也称为“嵌套”)中的顶级支持。要为所有子路由提供自定义的 Scaffold,请使用 scaffoldBuilder。例如,一个经典的“标签样式”应用程序可能看起来像这样:

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(...)) ]),
      ]);}
}

此外,你可以嵌套 PathStack Widget 来创建子部分。每个子部分都有自己的 scaffold。例如,这里我们为应用程序的“/settings/”部分的所有路由包装了一个嵌套的 scaffold。

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(),
},);},));

路径解析规则

有许多规则决定了路径如何路由:

  • 没有尾部斜杠的路由必须精确匹配。
    • 例如,/details 只匹配 /details,而不匹配 /details//details/12/details/?id=12
    • / 被认为是一个特殊情况,总是精确匹配。
  • 带有尾部斜杠的路由将接受一个后缀,
    • 例如,/details/ 匹配 /details//details/12/details/id=12&foo=99 等。
    • 这允许无限级别的嵌套和相对路由。
  • 如果路由有多个路径,只有第一个路径会被考虑用于后缀检查。
    • 例如,["/details", "/details/"] 要求精确匹配其中一个路径。
    • 例如,["/details/", "/details"] 允许其中一个路径接受后缀。

定义路径和查询字符串参数

路径参数(/billing/88/99)和查询字符串参数(/billing/?foo=88&bar=99)都受支持。

为了在参数进入你的视图之前对其进行解析,你可以使用 StackRouteBuilder()
消耗基于路径的参数如下所示:

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

消耗查询字符串参数如下所示:

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

如果你想在视图中访问参数并进行解析,你可以这样做:

NavStack.of(context).args;

有关路径解析的更多信息,请查看 https://pub.dev/packages/path_to_regexp
要尝试不同的路由方案,你可以使用此演示:https://path-to-regexp.web.app/

命令式 API

NavStack 提供了一个强大的命令式 API 来与你的导航状态进行交互。

  • NavStackController 可以随时使用 NavStack.of(context) 进行查找。
  • navStack.path 用于更改全局路由路径。
  • navStack.history 用于访问到目前为止的路径条目历史记录,你可以根据需要修改并重新分配此列表。
  • navStack.goBack() 用于向后移动一级历史记录。
  • navStack.popUntil()navStack.popMatching()navStack.replacePath() 等。

保持传统

重要的是,你仍然可以充分利用旧的 Navigator.push()showDialogshowBottomSheet API,但请注意,这些路由中的任何一个都不会反映在导航路径中。这对于不需要绑定到浏览器历史记录的用户流程来说非常有用。

重要: 整个 NavStack 存在于单个 PageRoute 中。这意味着从 NavStack 的子项调用 Navigator.of(context).pop() 将被忽略。但是,你仍然可以使用 .pop() 从对话框、底部工作表或使用 Navigator.push() 触发的全屏 PageRoutes 中弹出它们。

? 高级用法

除了基本的嵌套和路由之外,NavStack 还支持高级功能,包括别名、正则表达式和路由守卫。

正则表达式

基于路径的参数的一个强大之处在于你可以为匹配项附加正则表达式。

  • 例如,路由 /user/:foo(\d+) 将匹配 '/user/12' 但不匹配 '/user/alice'。
  • 如果你不知道正则表达式,不用担心,它们是可选的,并且最适合高级用例。

有关此解析的更多详细信息,请查看 PathToRegExp 文档。
https://pub.dev/packages/path_to_regexp

别名

每个路由条目可以有多个路径,允许它匹配其中任何一个。例如,我们可以设置一个路由来匹配 /home/

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

或者一个接受可选命名参数的路由。

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

路由守卫

守卫允许你对每个路由进行导航事件的拦截。通常用于阻止未经授权的应用程序部分的深层链接。

要做到这一点,你可以使用 StackRouteBuilder.onBeforeEnter 回调来运行你自己的自定义逻辑,并决定是否阻止更改。

例如,此守卫将重定向到 LoginScreen 并显示一个警告对话框(但你可以做任何你想做的事情)。

// 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;
}

由于守卫只是函数,你可以轻松地在路由之间重用它们,并且可以通过嵌套 PathStack 组件将它们应用于整个部分。

整合

这是一个更完整的示例,展示了嵌套堆栈以及一个需要用户登录的整个部分。否则,他们将被重定向到 /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

注意:此处使用字符串字面量("/home")是为了简洁和清晰。在实际使用中,建议为每个页面提供自己的路径属性,如 HomePage.pathLoginScreen.path。这使得构建和共享来自应用程序其他部分的链接更加容易:controller.path = "${SettingsPage.path}${ProfilePage.path}$profileId"

你可以为 PathStack 提供许多其他选项,包括 unknownPathBuildertransitionBuilderbasePath。有关详尽列表,请查看此示例。

GitHub

https://github.com/gskinnerTeam/flutter-nav-stack