Flutter路由管理插件hyper_router的使用

发布于 1周前 作者 bupafengyu 来自 Flutter

Flutter路由管理插件hyper_router的使用

img

HYPER_ROUTER

HYPER_ROUTER 是一个声明式的、类型安全的、无需代码生成的 Flutter 路由管理插件。

特点

  • 基于值的导航
  • 声明式
  • 路由守卫
  • 嵌套路由与状态保存
  • 从路由返回值
  • 可选的 URL 支持
  • 高度可扩展
  • 无需代码生成

概述

  1. 声明路由树:每个节点都有一个唯一的键。
  2. 访问控制器:使用 HyperRouter.of(context)context.hyper 与路由器交互。
  3. 导航:使用 context.hyper.navigate(<key>) 推送新路由到栈中。
  4. 弹出路由:使用 context.hyper.popNavigator.of(context).pop 返回上一个屏幕。

路由树配置

final router = HyperRouter(
  initialRoute: HomeScreen.routeName,
  routes: [
    ShellRoute(
      shellBuilder: (context, controller, child) =>
          MainTabsShell(controller: controller, child: child),
      tabs: [
        NamedRoute(
          screenBuilder: (context) => const HomeScreen(),
          name: HomeScreen.routeName,
          children: [
            NamedRoute(
              screenBuilder: (context) => const ProductListScreen(),
              name: ProductListScreen.routeName,
              children: [
                ValueRoute<ProductRouteValue>(
                  screenBuilder: (context, value) =>
                      ProductDetailsScreen(value: value),
                ),
              ],
            ),
          ],
        ),
        NamedRoute(
          screenBuilder: (context) => const GuideScreen(),
          name: GuideScreen.routeName,
        ),
        NamedRoute(
          screenBuilder: (context) => const SettingsScreen(),
          name: SettingsScreen.routeName,
        ),
      ],
    ),
  ],
);

常见路由类型

  • NamedRoute:基本路由,关联一个唯一名称。
  • ValueRoute<T>:允许传递数据到另一个屏幕的路由。
  • ShellRoute:包含嵌套导航器的路由,例如底部导航栏。

NamedRoute

用于简单的屏幕间导航,不需要传递数据。

  1. 声明路由名称
class HomeScreen extends StatelessWidget {
  static const routeName = RouteName('home');
  // ...
}
  1. 导航
HyperRouter.of(context).navigate(HomeScreen.routeName); 
// 或者,为了方便:
// context.hyper.navigate(HomeScreen.routeName);

ValueRoute<T>

如果需要在导航时传递数据,使用 ValueRoute<T>

  1. 声明值类型
class ProductRouteValue extends RouteValue {
  const ProductRouteValue(this.product);
  final Product product;
}
  1. 导航
context.hyper.navigate(ProductRouteValue(
  Product(/*...*/)
)); 

ShellRoute

用于创建底部导航栏。

  • 参数

    • shellBuilder:包裹子路由并显示标签栏的屏幕。
    • tabs:将显示在壳内的路由。
  • 使用 ShellController

    • setTabIndex(index):切换到指定索引的标签。
    • tabIndex:获取当前活动标签的索引。

示例:

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

class TabsShell extends StatelessWidget {
  const TabsShell({
    required this.controller,
    required this.child,
    super.key,
  });

  final Widget child;
  final ShellController controller;

  @override
  Widget build(BuildContext context) {
    final i = controller.tabIndex;

    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        onDestinationSelected: (value) {
          controller.setTabIndex(value);
        },
        selectedIndex: controller.tabIndex,
        destinations: [
          NavigationDestination(
            icon: Icon(i == 0 ? Icons.home_outlined : Icons.home),
            label: "Home",
          ),
          NavigationDestination(
            icon: Icon(i == 1 ? Icons.shopping_bag_outlined : Icons.shopping_bag),
            label: "Cart",
          ),
          NavigationDestination(
            icon: Icon(i == 2 ? Icons.settings_outlined : Icons.settings),
            label: "Settings",
          ),
        ],
      ),
    );
  }
}

从路由返回值

  • 接收结果
final result = await context.hyper.navigate(FormScreen.routeName);
  • 返回结果
// FormScreen
context.hyper.pop(value);

原生的 Flutter pushpop 也适用。例如,显示对话框:

final result = await showDialog(Dialog(...));
Navigator.of(context).pop(value);

守卫

使用重定向回调来控制基于条件(如认证状态)的导航流:

final router = HyperRouter(
  redirect: (context, state) {
    final authCubit = context.read<AuthCubit>();

    // 检查用户是否未登录且尝试访问认证路由
    if (!authCubit.state.authenticated &&
        state.stack.containsNode(AuthwalledScreen.routeName.key)) {
      return AuthScreen.routeName; // 重定向到认证
    }

    return null; // 不需要重定向
  },
  // ...
);
  • state.stack:表示即将的导航栈。第一个元素在底部,最后一个在顶部。
  • stack.containsNode:检查具有所提供键的路由是否存在。注意它需要显式地提供 key
  • 返回
    • 要重定向用户的路由键。这是与 navigate 相同的值。
    • null,如果不需要重定向。

启用 URL

有两个用例需要 URL 支持:Web 应用和深度链接。由于大多数 Flutter 应用针对移动平台,且深度链接通常只覆盖少数目的地,HYPER_ROUTER 设计为使 URL 支持可选。

Web

默认情况下,应用在浏览器中运行良好,但 URL 不会更新。要修复这一点,将 enableUrl 属性设置为 true

final router = HyperRouter(
  enableUrl: true,
  // ...
);

现在,你需要确保每个路由都可以解析为 URL 段。NamedRoute 默认支持解析,但 ValueRoute 需要提供解析器。

创建 URL 解析器

这里我们为 ProductRouteValue 创建一个解析器。我们希望 URL 看起来像这样:home/products/<productID>。解析器负责 <productId> 段:

class ProductSegmentParser extends UrlSegmentParser<ProductRouteValue> {
  @override
  ProductRouteValue? decodeSegment(SegmentData segment) {
    return ProductRouteValue(segment.name);
  }

  @override
  SegmentData encodeSegment(ProductRouteValue value) {
    return SegmentData(name: value.productId);
  }
}

你可以选择性地将查询参数提供给 SegmentDataqueryParams 字段)。它们将放在 URL 的末尾。如果堆栈中包含多个带有查询参数的路由,它们将被合并。

decodeSegment 应该在不识别段时返回 null

segment.state 存储在浏览器的历史记录中。你可以将不想在 URL 中显示的数据放在这里,当用户使用浏览器的后退和前进按钮导航时,这些数据将被恢复。

深度链接

TODO(尽管可能已经实现)

创建自定义路由

我设计了这个包,使其高度可扩展,以便为任何奇怪和不寻常的用例创建路由。例如,在 demo app 中,我创建了一个响应式的 list-detail 视图:在宽屏上显示列表和详细页面并排显示(类似于壳路由),在小屏幕上按顺序显示。

路由器的工作原理:

  1. 从目标路由到根遍历路由树,构建一个 RouteNode 的链表。
  2. 每个节点负责构建自己的页面及其所有后续页面。这发生在 createPages 方法中,该方法返回页面列表。
    • NamedRouteValueRoute 只是将自己的页面和所有后续页面堆叠在一起:
      Iterable<Page> createPages(BuildContext context) {
        final page = buildPage(context);
        return [page].followedByOptional(next?.createPages(context));
      }
      
    • ShellRoute 只将自己的页面放入列表,而其所有子路由则放入壳页面内的嵌套导航器中。
  3. 要创建自定义路由,需要覆盖这两个类:HyperRouteRouteNode

这应该足以理解 demo app 中的示例。

示例代码

import 'package:example/features/demos/guard/auth_screen.dart';
import 'package:example/features/demos/guard/authwalled_screen.dart';
import 'package:example/features/demos/guard/state/auth_cubit.dart';
import 'package:example/features/demos/guard/state/auth_state.dart';
import 'package:example/features/navigation/router.dart';
import 'package:example/features/utils/material_match.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (context) => AuthCubit()),
      ],
      child: BlocListener<AuthCubit, AuthState>(
        listener: (context, state) {
          final c = router.controller;

          if (!state.authenticated &&
              c.stack.containsNode(AuthwalledScreen.routeName.key)) {
            c.navigate(AuthRouteValue(c.stack.last().value));
          }
        },
        child: MaterialApp.router(
          theme: _createTheme(),
          routerConfig: router,
        ),
      ),
    );
  }

  ThemeData _createTheme() {
    final theme = FlexColorScheme.dark(
      useMaterial3: true,
      scheme: FlexScheme.deepPurple,
    ).toTheme;

    return theme.copyWith(
      cardTheme: CardTheme(
        margin: const EdgeInsets.all(8),
        color: theme.colorScheme.surfaceVariant,
        clipBehavior: Clip.antiAlias,
        elevation: 0,
        shape: RoundedRectangleBorder(
          side: BorderSide.none,
          borderRadius: BorderRadius.circular(32),
        ),
      ),
      inputDecorationTheme: InputDecorationTheme(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(32),
        ),
        contentPadding: const EdgeInsets.symmetric(horizontal: 32),
        hoverColor: theme.colorScheme.tertiaryContainer,
        filled: true,
      ),
      textButtonTheme: TextButtonThemeData(
        style: ButtonStyle(
          textStyle: materialMatch(
            all: const TextStyle(
              fontSize: 16,
            ),
          ),
          padding: materialMatch(all: const EdgeInsets.all(24)),
          splashFactory: InkSparkle.splashFactory,
        ),
      ),
      iconButtonTheme: const IconButtonThemeData(
        style: ButtonStyle(splashFactory: InkSparkle.splashFactory),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: ButtonStyle(
          textStyle: materialMatch(
            all: const TextStyle(
              fontSize: 16,
            ),
          ),
          padding: materialMatch(all: const EdgeInsets.all(24)),
          splashFactory: InkSparkle.splashFactory,
        ),
      ),
      filledButtonTheme: FilledButtonThemeData(
        style: ButtonStyle(
          textStyle: materialMatch(
            all: const TextStyle(
              fontSize: 16,
            ),
          ),
          padding: materialMatch(all: const EdgeInsets.all(24)),
          splashFactory: InkSparkle.splashFactory,
        ),
      ),
      dialogTheme: DialogTheme(
        actionsPadding: const EdgeInsets.all(32),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(32),
        ),
      ),
    );
  }
}

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

1 回复

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


当然,下面是一个关于如何在Flutter项目中使用hyper_router进行路由管理的示例代码。hyper_router是一个强大的路由管理插件,它提供了灵活的路由配置和状态管理功能。

首先,确保你已经在pubspec.yaml文件中添加了hyper_router依赖:

dependencies:
  flutter:
    sdk: flutter
  hyper_router: ^x.y.z  # 请将x.y.z替换为最新的版本号

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

接下来,让我们创建一个简单的Flutter项目,并使用hyper_router来管理路由。

1. 初始化HyperRouter

main.dart文件中,首先初始化HyperRouter

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HyperRouter(
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context, {required String id}) => DetailsScreen(id: id),
      },
      initialRoute: '/',
      builder: (context, widget) => MaterialApp(
        home: widget,
        navigatorKey: HyperRouter.navigatorKey, // 用于全局导航
      ),
    );
  }
}

2. 创建HomeScreen和DetailsScreen

接下来,创建两个简单的屏幕:HomeScreenDetailsScreen

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

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 导航到详情页面,并传递一个id参数
            HyperRouter.of(context).navigate('/details', arguments: {'id': '123'});
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

class DetailsScreen extends StatelessWidget {
  final String id;

  DetailsScreen({required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Screen'),
      ),
      body: Center(
        child: Text('Details for ID: $id'),
      ),
    );
  }
}

3. 使用全局导航(可选)

如果你需要在应用的任何地方进行全局导航,可以使用HyperRouter.navigatorKey。例如,你可以在MyApp的顶部添加一个全局按钮来演示这一点:

class MyApp extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return HyperRouter(
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context, {required String id}) => DetailsScreen(id: id),
      },
      initialRoute: '/',
      builder: (context, widget) => MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter App'),
            actions: [
              IconButton(
                icon: Icon(Icons.navigate_next),
                onPressed: () {
                  navigatorKey.currentState?.pushNamed('/details', arguments: {'id': '456'});
                },
              ),
            ],
          ),
          body: widget,
        ),
        navigatorKey: navigatorKey, // 用于全局导航
      ),
    );
  }
}

请注意,在上面的示例中,我们将navigatorKey定义在MyApp类中,并将其传递给MaterialAppnavigatorKey属性。这样,你就可以在应用的任何地方使用这个navigatorKey来进行全局导航。

总结

通过上述步骤,你已经成功地在Flutter项目中使用hyper_router进行了路由管理。这个插件提供了强大的功能,可以帮助你轻松地管理应用的路由和状态。如果你需要更高级的功能,比如嵌套路由、守卫(guard)、动画等,请参考hyper_router的官方文档。

回到顶部