Flutter路由管理插件theseus_navigator的使用

Flutter路由管理插件theseus_navigator的使用

概述

使用这些us导航器的起点是定义你的应用的导航方案。它可能看起来像这样:

NavigationScheme

目的地(Destination) 定义了用户在你的应用中可以到达的所有可能的UI端点。

导航控制器(NavigationController) 负责在其目的地范围内管理应用的导航状态。它执行导航操作,如 goTo(destination)goBack(),并构建导航堆栈。

导航方案(NavigationScheme) 是导航的入口点,它协调所有目的地和导航控制器。它有一个根导航控制器来管理顶级目的地,并且可选地具有额外的导航器以支持嵌套导航。

以下是使用示例:

定义目的地和导航方案

final homeDestination = Destination(
  path: 'home',
  isHome: true,
  builder: (context, parameters) => HomeScreen(),
);

final catalogDestination = Destination(
  path: 'catalog',
  builder: (context, parameters) => CatalogScreen(),
);

final settingsDestination = Destination(
  path: 'settings',
  builder: (context, parameters) => SettingsScreen(),
); 

final navigationScheme = NavigationScheme(
  destinations: [
    homeDestination,
    catalogDestination,
    settingsDestination,
  ],
);

设置带有导航方案的路由器

@override
Widget build(BuildContext context) {
  return MaterialApp.router(
    //...
    routerConfig: navigationScheme.config,
  );
}

在应用中的某个位置进行导航

onTap: () => navigationScheme.goTo(ordersDestination)

目的地(Destination)

目的地(Destination) 是一个模型,表示用户可以在你的应用中导航到的UI端点。

通常,你定义一个目的地如下:

final homeDestination = Destination(
  path: 'home',
  builder: (context, parameters) => HomeScreen(),
);

这是一个 最终目的地(final destination),它直接显示由提供的 builder 函数返回的内容。

嵌套导航(Nested Navigation)

对于嵌套导航,你应该提供一个 导航器(navigator),这应该是一个 NavigationController。它管理自己的目的地,这些目的地可以是最终的,构建内容或过渡的,提供另一个嵌套导航器。

final mainDestination = Destination(
  path: '/',
  navigator: mainNavigator,
);

final mainNavigator = NavigationController(
  destinations:[
    homeDestination,
    catalogDestination,
    settingsDestination,
  ],
);

通过使用 Destination.transit() 构造函数,还可以将嵌套导航UI包装在一些额外的widget子树中。

final mainDestination = Destination.transit(
  path: '/',
  navigator: mainNavigator,
  builder: (context, parameters, childBuilder) {
    return Column(
      children: [
        const Text('Parent destination'),
        Expanded(child: childBuilder(context)),
      ],  
    );
  }
);

此构造函数有一个可选的 builder 参数,其中包含一个表示嵌套内容的 childBuilder 参数,必须包含在生成的widget树中。

路径(Path)

目的地由其 uri 定义,该 uri 由目的地的 pathparameters 构建。

路径可能包含用于路径参数的占位符。最后一个路径参数是可选的。还支持任意查询参数。

例如,目的地路径为:

/categories/{categoryId}

以下特定URI将匹配该目的地路径:

/categories
/categories/1
/categories?q=someQuery
/categories/1?q=someQuery

最后两个URI包含查询参数,但仍匹配指定的目的地路径。

参数(Parameters)

默认参数处理(Default Parameters Handling)

默认情况下,目的地参数(包括路径和查询)从目的地的URI提取到 DestinationParameters 类中。它们存储为 Map<String, String>

在这种情况下,你不需要指定一个目的地解析器,隐式使用 DefaultDestinationParser 实现。

final categoriesDestination = Destination(
   path: 'categories',
   builder: (context, params) => CategoryListScreen(
       parentCategoryId: params?['parentId'],
   ),
 );

例如,上述 URI categories?parentId=2 将被解析为一个目的地对象,该对象会构建一个 CategoryListScreen 并带有 parentCategoryId: 2

自定义类型参数(Custom Type Parameters)

要使用具有特定类型的参数的目的地,你需要做以下几件事:

  • 创建一个扩展 DestinationParameters 的类,如下所示:
class CategoriesDestinationParameters extends DestinationParameters {
  CategoriesDestinationParameters({
    this.parentCategory,
  }) : super();

  final Category? parentCategory;
}
  • 实现特定于你的参数类型的解析器:
class CategoriesDestinationParser extends DestinationParser<CategoriesDestinationParameters> {
  CategoriesDestinationParser({
    required this.categoryRepository,
  });

  final CategoryRepository categoryRepository;

  @override
  Future<CategoriesDestinationParameters> parametersFromMap(Map<String, String> map) async {
    final category = await categoryRepository.getCategory(map['parentCategoryId'] ?? '');
    return CategoriesDestinationParameters(
      parentCategory: category,
    );
  }

  @override
  Map<String, String> parametersToMap(CategoryListParameters parameters) {
    final result = <String, String>{};
    if (parameters.parentCategory != null) {
      result['parentCategoryId'] = parameters.parentCategory!.id;
    }
    return result;
  }
}
  • 创建你的目的地如下:
final categoriesDestination = Destination<CategoriesDestinationParameters>(
    path: 'categories',
    builder: (context, params) => CategoryListScreen(
        parentCategory: params?.parentCategory,
    ),
    parser: CategoriesDestinationParser(
      categoryRepository: CategoryRepository(),
    ),
  );

设置(Settings)

目的地设置(DestinationSettings) 包含用于确定更新导航状态的逻辑和行为的属性。

transitionMethod - 定义导航到目的地时如何更改导航堆栈,可以是 pushreplace

transition - 当目的地内容出现时应用的动画,可以是 materialcustomnone

如果使用 custom 过渡,则还需要提供 transitionBuilder

有两个预定义的工厂方法:

  • material() - 返回将目的地推送到堆栈的标准Material动画。
  • dialog() - 将目的地作为模态对话框显示。
  • quiet() - 用新目的地替换当前目的地而不使用任何动画。
final catalogDestination = Destination(
  path: 'catalog',
  builder: (context, parameters) => CatalogScreen(),
  settings: const DestinationSettings.quiet(),
);

导航控制器(NavigationController)

导航控制器(NavigationController) 是包的核心组件,负责管理导航状态。

它维护导航堆栈中的目的地,并提供更新堆栈的方法,如 goTo(destination)goBack()

导航控制器是一个 ChangeNotifier,并在其导航堆栈的任何更新时通知 NavigationScheme

你可以访问整个导航堆栈和堆栈顶部的目的地使用 currentDestination 属性。

此示例显示了创建一个将目的地包装在标准Flutter Navigator widget中的导航器:

final mainNavigator = NavigationController(
  destinations: [
    homeDestination,
    catalogDestination,
    settingsDestination,
  ],
  tag: 'Main',
);

tag 属性值用作 Navigator widget的键,并且也允许找到与此 NavigationController 实例相关的日志。

导航构建器(Navigator Builder)

导航控制器允许你使用自定义导航UI包装目的地。

当你希望通过 BottomNavigationBarTabBarDrawer 或其他方式导航目的地时,这是必需的。

为此,你必须扩展 NavigatorBuilder 类并覆盖其 build 方法:

class CustomNavigatorBuilder extends NavigatorBuilder {
  const CustomNavigatorBuilder() : super();

  @override
  Widget build(BuildContext context, NavigationController navigator) {
    // 你的wrapper widget实现在这里。
    // 你可以访问导航器的堆栈和当前目的地。
  }
}

然后你应该在导航器实例中指定 builder

final mainNavigator = NavigationController(
  destinations: [
    homeDestination,
    catalogDestination,
    settingsDestination,
  ],
  builder: CustomNavigatorBuilder(),
  tag: 'Main',
);

通过底部导航栏、抽屉和标签栏进行导航

包包括最常见的 NavigatorBuilder 实现:

  • BottomNavigationBuilder - 使用Flutter的 ScaffoldBottomNavigationBar 或Material 3 NavigationBar 来包装当前目的地内容并切换目的地。
  • DrawerNavigationBuilder - 允许使用 Drawer widget进行导航。
  • TabsNavigationBuilder - 使用 TabBar 进行导航目的地。

例如,向你的应用添加底部导航栏非常简单:

final navigationScheme = NavigationScheme(
  navigator: NavigationController(
    destinations: [
      homeDestination,
      catalogDestination,
      settingsDestination,
    ],
    builder: BottomNavigationBuilder(
      bottomNavigationItems: const [
        BottomNavigationBarItem(
          icon: Icon(Icons.home_rounded),
          label: 'Home',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.list_rounded),
          label: 'Catalog',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.more_horiz_rounded),
          label: 'Settings',
        ),
      ],
    ),
    tag: 'Main',
  ), 
);

BottomNavigationBar widget的样式可以通过 BottomNavigationBuilder 的可选 parameters 参数来支持。

你可以以相同的方式使用 DrawerNavigationBuilderTabsNavigationBuilder

向上导航(Upward Navigation)

有时,在从用户跳过底层目的地的某个目的地反向导航时,我们需要恢复缺失的目的地层次结构。

例如,用户通过深度链接打开应用,该链接导致某个类别屏幕位于类别层次结构中。从该屏幕导航回时,我们希望显示上级类别屏幕,依此类推直到类别根目录。

当为目的地定义 upwardDestinationBuilder 参数时,包支持这种行为。

它可能看起来像这样:

final categoriesDestination = Destination<CategoryListParameters>(
  path: 'categories',
  builder: (context, params) => CategoryListScreen(
      parentCategory: params?.category,
  ),
  upwardDestinationBuilder: (destination) =>
    destination.parameters?.parentCategory == null
        ? null
        : destination.copyWithParameters(CategoriesDestinationParameters(
            parentCategory:
                destination.parameters?.parentCategory!.parent)),
  parser: CategoriesDestinationParser(
    categoryRepository: CategoryRepository(),
  ),
);

深度链接(Deep-links)

当平台请求一个目的地时,对于从当前目的地到根的每个导航器层次结构,将发生以下情况:

  • 当前目的地根据请求的深度链接目的地进行更新。
  • 如果当前目的地提供了 upwardDestinationBuilder 参数,则在添加新目的地之前清空导航器堆栈。否则,当前目的地将被推送到现有堆栈中。

重定向(Redirections)

有时我们需要在显示请求的内容之前将用户重定向到另一个屏幕。

基本示例是某些屏幕只应显示给已登录的用户。

包提供了 Redirection 类以支持此行为。你可以为应在导航之前验证的目的地指定一个重定向列表。

final settingsDestination = Destination(
      path: 'settings',
      builder: (context, parameters) => SettingsScreen(),
      redirections: [
        Redirection(
          validator: (destination) => SynchronousFuture(isLoggedIn),
          destination: loginDestination,
        )
      ]
    ); 

在上述示例中,当用户导航到设置屏幕时,首先将调用指定重定向的 validator 函数。如果它返回 false,则用户将被重定向到登录屏幕。

重定向中的验证函数可以是异步的。如果它运行时间较长,等待覆盖小部件将在目标解决之前显示。你可以通过提供 NavigationSchemewaitingOverlayBuilder 参数来自定义等待覆盖小部件。

也可以通过提供 resolver 函数而不是固定的 destination 动态识别重定向的目的地。

错误处理(Error Handling)

如果在 NavigationScheme 中指定了 errorDestination,则当尝试导航到不存在的屏幕时,用户将被重定向到此屏幕。

final navigationScheme = NavigationScheme(
  destinations: [
    //...
  ],
  errorDestination: Destination(
    path: '/error',
    builder: (context, parameters) => ErrorScreen(),
  )
);

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

1 回复

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


当然,以下是一个关于如何使用Flutter路由管理插件theseus_navigator的代码示例。theseus_navigator是一个功能强大的路由管理插件,它支持多种导航模式,并且易于集成到Flutter应用中。

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter
  theseus_navigator: ^最新版本号

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

2. 配置路由

接下来,你需要配置路由。在一个新的Flutter项目中,你可以在lib目录下创建一个router文件夹,并在其中创建一个router.dart文件来配置路由。

// lib/router/router.dart

import 'package:flutter/material.dart';
import 'package:theseus_navigator/theseus_navigator.dart';
import 'package:your_app/screens/home_screen.dart';
import 'package:your_app/screens/detail_screen.dart';

class AppRouter {
  static final navigatorKey = GlobalKey<NavigatorState>();

  static final routes = Routes(
    routes: {
      '/': (context) => HomeScreen(),
      '/detail': (context, {required String id}) => DetailScreen(id: id),
    },
  );

  static MaterialApp getMaterialApp(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: routes.routeInformationParser,
      routerDelegate: routes.routerDelegate.withNavigator(navigatorKey),
    );
  }
}

3. 创建屏幕

然后,你需要创建一些屏幕组件,比如HomeScreenDetailScreen

// lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:theseus_navigator/theseus_navigator.dart';
import 'package:your_app/router/router.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Navigate to detail screen with an example ID
            AppRouter.routes.navigator.pushNamed(
              '/detail',
              arguments: {'id': '123'},
            );
          },
          child: Text('Go to Detail'),
        ),
      ),
    );
  }
}
// lib/screens/detail_screen.dart

import 'package:flutter/material.dart';
import 'package:your_app/router/router.dart';

class DetailScreen extends StatelessWidget {
  final String id;

  DetailScreen({required this.id});

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

4. 使用路由

最后,在你的main.dart文件中使用配置好的路由。

// lib/main.dart

import 'package:flutter/material.dart';
import 'package:your_app/router/router.dart';

void main() {
  runApp(AppRouter.getMaterialApp(context: null)); // Note: context is null here because we're not using it directly
}

注意:在main.dart中传递contextAppRouter.getMaterialApp时,由于main函数的上下文还没有被创建,所以直接传递null或者使用其他方式初始化(例如通过builder参数)。然而,theseus_navigator通常不需要在main函数中直接处理context,因为它使用GlobalKey来管理导航器状态。

5. 运行应用

现在,你可以运行你的Flutter应用,点击Home Screen上的按钮应该会导航到Detail Screen并显示传递的ID。

这是一个基本的示例,展示了如何使用theseus_navigator进行路由管理。根据你的具体需求,你可以进一步扩展和自定义路由配置。

回到顶部