Flutter导航管理插件nav_stack的使用

Flutter导航管理插件nav_stack的使用

nav_stack

一个基于MaterialApp.router(Nav 2.0)的简单但强大的基于路径的路由系统。它支持浏览器/深度链接,并在添加新路由时维护历史堆栈。此外,它还提供了灵活的命令式API来更改路径和修改历史堆栈。

安装

dependencies:
  nav_stack: ^0.0.1

导入

import 'package:nav_stack/nav_stack.dart';

基本用法

Hello NavStack

要开始,请将NavStackParser()NavStackDelegate()传递给MaterialApp.router,并在onGenerateStack回调中声明所有路由。

runApp(
  MaterialApp.router(
    routeInformationParser: NavStackParser(),
    routerDelegate: NavStackDelegate(
      // 声明你的全树页面路由
      // PathStack会自动根据当前导航路径决定显示什么
      onGenerateStack: (context, nav) => PathStack(
        routes: {
          ["/"]: HomeScreen().toStackRoute(), // 主页
          ["/messages"]: MessagesScreen().toStackRoute(), // 消息页面
          ["/profile"]: ProfileScreen().toStackRoute(), // 个人资料页面
        },
      ),
    ));
});

这段代码可能看起来很简单,但实际上包含了很多功能:

  • 这完全绑定到浏览器路径,
  • 它还会接收任何平台上的深度链接启动值,
  • 它提供了一个nav控制器,可以随时轻松更改全局路径,
  • 你可以从任何地方通过NavStack.of(context)查找它,
  • 所有路由都是持久的,在导航之间保持状态(可选)。

在这个基本形式下,这给你提供了类行为的路由表,使用MaterialApp.onGenerateRoute构建。

如何工作

NavStack实际上是两个独立组件的组合。

  • NavStack库与MaterialApp.router通信,并提供读写全局导航路径的API。
  • 然后是PathStack,它解析并根据当前导航路径渲染路由。

PathStack类似于使用字符串而不是整数作为键的IndexedStack

  • 它支持许多额外的功能,如嵌套支架、自定义过渡、相对路径和参数。
  • 你可以将多个PathStack嵌套在一起以轻松形成复杂的路由表。
  • 类似于IndexedStackPathStack的子项可以是持久的。
  • 在底层,它实际上只是一个IndexedStack和一个Map<String, Widget>

有关PathStack的更多文档,请参阅:https://pub.dev/packages/path_stack

嵌套路由和堆栈

为了在所有子路由周围包裹一个自定义的Scaffold或菜单,可以使用scaffoldBuilder。一种常见的应用是一个具有持久标签菜单的应用程序:

onGenerateStack: (_, __) => PathStack(
  // 使用scaffoldBuilder将所有页面包装在一个状态化的标签菜单中
  scaffoldBuilder: (_, stack) => _TabScaffold(["/home", "/profile"], child: stack),
  routes: {
    ["/home"]: LoginScreen().toStackRoute(), // 登录页面
    ["/profile"]: ProfileScreen().toStackRoute(), // 个人资料页面
  },
));

如果你想在特定的一组页面周围包裹一个Scaffold,只需创建另一个PathStack

这里我们通过结合两个堆栈来围绕应用程序的/settings/部分包裹一个内部标签菜单:

onGenerateStack: (_, __) {
  return PathStack( // 外部Scaffold
    scaffoldBuilder: (_, stack) => OuterTabScaffold(stack),
    routes: {
      ["/login", "/"]: LoginScreen().toStackRoute(), // 登录页面
      ["/settings/"]: PathStack( // 内部Scaffold
        scaffoldBuilder: (_, stack) => InnerTabScaffold(stack),
        routes: {
          // 这将匹配"/settings/profile"。默认情况下,所有路由相对于其父PathStack。
          ["profile"]: ProfileScreen().toStackRoute(),
          ["alerts"]: AlertsScreen().toStackRoute(),
        },
      ).toStackRoute(),
    },
  );
},

当你在/settings部分内更改路由时,两个Scaffolds都会保持不变,只有内部区域会动画!Scaffolds也是完全状态化的,因此当路由更改时,你可以拥有动画和其他装饰效果。

路径解析规则

路径路由有一些规则:

  • 不带尾随斜杠的路由必须完全匹配:
    • 例如,/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)的参数。

一个消费基于路径参数的路由看起来像这样:

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

一个使用查询字符串参数的路由看起来像这样:

// /billing/?foo=88&bar=99
["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/

toStackRoute vs StackRouteBuilder

PathStack的一个要求是每个页面Widget都必须包装在一个StackRouteBuilder()中。由于这可能会难以阅读,我们添加了一个.toStackRoute()扩展方法。两者之间的唯一区别是,完整的StackRouteBuilder允许你直接使用它的builder方法注入参数到你的视图中。

如果你的视图不需要路径参数,考虑使用扩展方法,因为它们通常更易读:

// 这些调用是相同的
["/login"]: LoginScreen().toStackRoute(),
VS
["/login"]: StackRouteBuilder(builder: (_, __) => LoginScreen()),

Navigator.of()呢?

重要的是,你仍然可以充分利用旧的Navigator.push()showDialogshowBottomSheetAPI,只是要注意这些路由不会反映在导航路径中。这对于不需要绑定到浏览器历史的用户流程非常有用。此外,Overlay仍然存在,并且表现得正如预期。

主要的区别现在是,Navigator主要用于覆盖整个应用的东西,而不是改变应用内的页面。

重要提示: 整个NavStack存在于单个PageRoute中。这意味着从NavStack子项中调用Navigator.of(context).pop()将被忽略。然而,你仍然可以在对话框、底部面板或通过Navigator.push()添加的页面中使用.pop()

高级用法

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

正则表达式

基于路径参数的一个强大方面是你可以在匹配中附加正则表达式。

  • 例如,路径/user/:foo(\d+)将匹配/user/12但不匹配/user/alice
  • 如果你不熟悉正则表达式,它们是可选的,并且最好用于高级用例

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

别名

每个路由条目可以有多个路径,使其能够匹配任意路径。例如,我们可以设置一个路由来匹配/home/

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

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

// 匹配"/messages/"和"/messages/99"
["/messages/", "/messages/:messageId"]:  StackRouteBuilder(
  builder: (_, args) => MessageView(args["messageId"] ?? "")
);

路由守卫

守卫允许你在每个路由的基础上拦截导航事件。主要用于防止未经授权的深链接进入应用程序的部分。通常,你可能会将所有主要页面包装在一个authGuard中,并留下LoginView未授权,或者你可能有一个受保护的授权部分,如/admin/

要做到这一点,你可以使用StackRouteBuilder.onBeforeEnter回调来运行你自己的自定义逻辑,并决定是否阻止更改。例如,这个守卫将保护AdminPage并将重定向到LoginScreen

// 你可以使用`buildStackRoute`或`StackRouteBuilder`来添加守卫
["/admin"]: AdminPanel().buildStackRoute(
  onBeforeEnter: (_, __) => guardAuthSection()
),
...
bool guardAuthSection(String newRoute) {
  if (!appModel.isLoggedIn) NavStack.of(context).redirect("/login");
  return appModel.isLoggedIn; // 如果返回false,原始路由将不会被进入。
}

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

组合使用

这是一个更完整的示例,展示了嵌套堆栈和一个需要用户登录的整个部分。否则,他们将被重定向到/login

bool isLoggedIn = false;

...

onGenerateStack: (context, controller) {
  return PathStack(
    scaffoldBuilder: (_, stack) => _MyScaffold(stack),
    routes: {
      ["/login", "/"]: LoginScreen().toStackRoute(),
      ["/in/"]: PathStack(
        routes: {
          ["profile/:profileId"]:
              StackRouteBuilder(builder: (_, args) => ProfileScreen(profileId: args["profileId"] ?? "")),
          ["settings"]: SettingsScreen().toStackRoute(),
        },
      ).toStackRoute(onBeforeEnter: (_) {
        if (!isLoggedIn) controller.redirect("/login");
        return isLoggedIn; // 如果返回false,该路由将不会被进入。
      }),
    },
  );
},
...

void handleLoginPressed() => NavStack.of(context).path = "/login";
void showProfile() => NavStack.of(context).path = "/in/profile/23"; // 受保护
void showSettings() => NavStack.of(context).path = "/in/settings"; // 受保护

更多关于Flutter导航管理插件nav_stack的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter导航管理插件nav_stack的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


nav_stack 是一个用于 Flutter 的导航管理插件,它提供了一种简单且灵活的方式来管理应用程序的导航栈。使用 nav_stack,你可以轻松地实现复杂的导航逻辑,例如嵌套导航、深层链接、以及基于状态的路由管理。

以下是如何使用 nav_stack 插件的基本指南:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 nav_stack 依赖:

dependencies:
  flutter:
    sdk: flutter
  nav_stack: ^0.1.0  # 请检查最新版本

然后运行 flutter pub get 来安装依赖。

2. 基本用法

nav_stack 提供了一个 NavStack 小部件,它可以用来管理导航栈。你可以在 NavStack 中定义多个路由,并根据需要切换它们。

import 'package:flutter/material.dart';
import 'package:nav_stack/nav_stack.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: NavStack(
        stackBuilder: (context, controller) {
          return StackEntry(
            path: '/',
            builder: (context) => HomeScreen(),
          );
        },
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            NavStack.of(context).push('/details');
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

class DetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            NavStack.of(context).pop();
          },
          child: Text('Go Back'),
        ),
      ),
    );
  }
}

3. 定义路由

你可以在 NavStack 中定义多个路由,并使用 StackEntry 来指定每个路由的路径和对应的页面。

NavStack(
  stackBuilder: (context, controller) {
    return StackEntry(
      path: '/',
      builder: (context) => HomeScreen(),
      children: [
        StackEntry(
          path: '/details',
          builder: (context) => DetailsScreen(),
        ),
      ],
    );
  },
)

4. 导航操作

你可以使用 NavStack.of(context) 来获取当前的导航控制器,并使用 pushpop 等方法来进行导航操作。

// 导航到新页面
NavStack.of(context).push('/details');

// 返回上一页
NavStack.of(context).pop();

5. 嵌套导航

nav_stack 支持嵌套导航,你可以在一个 NavStack 中嵌套另一个 NavStack,从而实现复杂的导航逻辑。

NavStack(
  stackBuilder: (context, controller) {
    return StackEntry(
      path: '/',
      builder: (context) => HomeScreen(),
      children: [
        StackEntry(
          path: '/nested',
          builder: (context) => NavStack(
            stackBuilder: (context, controller) {
              return StackEntry(
                path: '/nested',
                builder: (context) => NestedHomeScreen(),
                children: [
                  StackEntry(
                    path: '/nested/details',
                    builder: (context) => NestedDetailsScreen(),
                  ),
                ],
              );
            },
          ),
        ),
      ],
    );
  },
)

6. 深层链接

nav_stack 还支持深层链接,你可以通过路径来直接导航到特定的页面。

NavStack(
  initialPath: '/nested/details',  // 初始化时直接导航到深层链接页面
  stackBuilder: (context, controller) {
    return StackEntry(
      path: '/',
      builder: (context) => HomeScreen(),
      children: [
        StackEntry(
          path: '/nested',
          builder: (context) => NavStack(
            stackBuilder: (context, controller) {
              return StackEntry(
                path: '/nested',
                builder: (context) => NestedHomeScreen(),
                children: [
                  StackEntry(
                    path: '/nested/details',
                    builder: (context) => NestedDetailsScreen(),
                  ),
                ],
              );
            },
          ),
        ),
      ],
    );
  },
)

7. 基于状态的路由管理

nav_stack 允许你基于应用程序的状态来管理路由。你可以使用 StackEntrycondition 参数来根据条件动态显示不同的页面。

NavStack(
  stackBuilder: (context, controller) {
    return StackEntry(
      path: '/',
      builder: (context) => HomeScreen(),
      children: [
        StackEntry(
          path: '/details',
          builder: (context) => DetailsScreen(),
          condition: () => someCondition,  // 根据条件决定是否显示该页面
        ),
      ],
    );
  },
)
回到顶部