Flutter路由管理插件hyper_router的使用
Flutter路由管理插件hyper_router的使用

HYPER_ROUTER
HYPER_ROUTER 是一个声明式的、类型安全的、无需代码生成的 Flutter 路由管理插件。
特点
- 基于值的导航
- 声明式
- 路由守卫
- 嵌套路由与状态保存
- 从路由返回值
- 可选的 URL 支持
- 高度可扩展
- 无需代码生成
概述
- 声明路由树:每个节点都有一个唯一的键。
- 访问控制器:使用 HyperRouter.of(context)或context.hyper与路由器交互。
- 导航:使用 context.hyper.navigate(<key>)推送新路由到栈中。
- 弹出路由:使用 context.hyper.pop或Navigator.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
用于简单的屏幕间导航,不需要传递数据。
- 声明路由名称:
class HomeScreen extends StatelessWidget {
  static const routeName = RouteName('home');
  // ...
}
- 导航:
HyperRouter.of(context).navigate(HomeScreen.routeName); 
// 或者,为了方便:
// context.hyper.navigate(HomeScreen.routeName);
ValueRoute<T>
如果需要在导航时传递数据,使用 ValueRoute<T>。
- 声明值类型:
class ProductRouteValue extends RouteValue {
  const ProductRouteValue(this.product);
  final Product product;
}
- 导航:
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 push 和 pop 也适用。例如,显示对话框:
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);
  }
}
你可以选择性地将查询参数提供给 SegmentData(queryParams 字段)。它们将放在 URL 的末尾。如果堆栈中包含多个带有查询参数的路由,它们将被合并。
decodeSegment 应该在不识别段时返回 null。
segment.state 存储在浏览器的历史记录中。你可以将不想在 URL 中显示的数据放在这里,当用户使用浏览器的后退和前进按钮导航时,这些数据将被恢复。
深度链接
TODO(尽管可能已经实现)
创建自定义路由
我设计了这个包,使其高度可扩展,以便为任何奇怪和不寻常的用例创建路由。例如,在 demo app 中,我创建了一个响应式的 list-detail 视图:在宽屏上显示列表和详细页面并排显示(类似于壳路由),在小屏幕上按顺序显示。
路由器的工作原理:
- 从目标路由到根遍历路由树,构建一个 RouteNode的链表。
- 每个节点负责构建自己的页面及其所有后续页面。这发生在 createPages方法中,该方法返回页面列表。- NamedRoute和- ValueRoute只是将自己的页面和所有后续页面堆叠在一起:- Iterable<Page> createPages(BuildContext context) { final page = buildPage(context); return [page].followedByOptional(next?.createPages(context)); }
- ShellRoute只将自己的页面放入列表,而其所有子路由则放入壳页面内的嵌套导航器中。
 
- 要创建自定义路由,需要覆盖这两个类:HyperRoute和RouteNode。
这应该足以理解 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
更多关于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
接下来,创建两个简单的屏幕:HomeScreen和DetailsScreen。
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类中,并将其传递给MaterialApp的navigatorKey属性。这样,你就可以在应用的任何地方使用这个navigatorKey来进行全局导航。
总结
通过上述步骤,你已经成功地在Flutter项目中使用hyper_router进行了路由管理。这个插件提供了强大的功能,可以帮助你轻松地管理应用的路由和状态。如果你需要更高级的功能,比如嵌套路由、守卫(guard)、动画等,请参考hyper_router的官方文档。
 
        
       
             
             
            

