【Flutter】GoRouter的路由管理 VS AutoRouter的路由管理

GoRouter 与 AutoRouter 的使用和对比

前言

在现代移动应用开发中,路由管理是一个至关重要的部分。一个良好的路由框架不仅能提高代码的可维护性,还能提升开发效率和用户体验。Flutter 作为一个跨平台框架,提供了多种路由解决方案,其中原生的 Navigator 和 Route 已经能够满足基本需求,但在实际项目中,往往需要更为强大和灵活的路由管理功能。

最近在重构一个新的框架时,我在路由选项上遇到了一些困惑。Flutter 的生态系统中有许多优秀的路由框架,其中 go_router 和 auto_route 是两款备受欢迎且使用频率较高的路由管理工具。为了选择最适合项目需求的路由框架,我决定对这两者进行深入比较和分析。

本文将详细记录 go_router 和 auto_route 的相关特点、使用方法,以及它们在不同场景中的优劣势,希望能为遇到相似问题的开发者提供一些参考和帮助。通过这篇文章,你将能够更好地理解这两种路由框架,并做出更明智的选择。

一、 go_router 与 auto_route 的异同点。

go_routerauto_route 都是建立在 Flutter Navigator 2.0 API 之上的路由库,它们的目标是简化 Flutter 应用中的路由和导航管理。它们都提供了声明式路由的方式和更好的深层链接支持。下面是两者的一些特点介绍与比较。

1.1 go_router 的特点
  1. 声明式 API : go_router 使用声明式 API,可以很容易地定义路由和它们的行为。

  2. 简化的路由定义: 它允许你通过简单的配置定义路由,使得路由表管理更加直观和集中。

  3. 路由守卫 : go_router 支持路由守卫(route guards),这允许开发者在路由跳转前执行权限检查或其他逻辑。

  4. 重定向: 支持重定向,可以根据业务逻辑动态改变目标路由。

  5. 易于理解 : 相对于 Navigator 2.0 的底层 API,go_router 的 API 更易于理解和使用。

1.2 auto_route 的特点
  1. 代码生成 : auto_route 通过代码生成来处理路由的创建,这使得路由表的维护更加类型安全和易于重构。

  2. 强类型导航 : 因为其生成的代码,auto_route 提供强类型的导航方法,降低了因为类型错误而产生的 bug。

  3. 嵌套路由 : auto_route 支持嵌套路由,非常适合构建复杂的应用界面结构。

  4. 页面包装器: 支持自定义页面包装器(wrappers),可以用于处理例如主题、语言等上下文相关的配置。

  5. 灵活性: 提供更多的灵活性来定义和管理路由,但同时带来的是更复杂的配置。

1.3 异同点

相同点:

  • 都是为了简化 Flutter Navigator 2.0 的路由管理。
  • 都支持深层链接处理和复杂的路由场景。
  • 都提供了路由守卫和重定向的功能。

不同点:

  • go_router 重在简化和去除样板代码,使用声明式 API。
  • auto_route 通过代码生成来创建强类型的路由,以此实现更严格的类型检查和路由管理。
  • auto_route 需要额外的步骤来生成代码,而 go_router 不需要。
  • auto_route 在嵌套路由和类型安全方面可能有更多的优势,而 go_router 更加侧重于简单和易用性。

二、 go_router 的使用

go_router 的使用不需要使用注解来配合 build_runner 自动化生成,我们手动的定义即可。

2.1 go_router 的集成与简单使用

在开始使用 go_router 之前,我们需要将其添加到项目的依赖中,并进行基本的集成配置。以下是具体步骤:

添加依赖:

在 pubspec.yaml 文件中添加 go_router 依赖:

yaml 复制代码
yaml
dependencies:
  go_router: ^14.3.0

创建一个 GoRouter 实例并配置路由:

php 复制代码
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
  final GoRouter _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => HomePage(),
      ),
      GoRoute(
        path: '/details/:id',
        builder: (context, state) => DetailsPage(id: state.params['id']!),
      ),
    ],
  );

  runApp(MyApp(router: _router));
}

class MyApp extends StatelessWidget {
  final GoRouter router;

  MyApp({required this.router});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: router.routerDelegate,
      routeInformationParser: router.routeInformationParser,
      routeInformationProvider: router.routeInformationProvider,
    );
  }
}

创建 HomePage 和 DetailsPage 作为示例页面:

scala 复制代码
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/details/1');
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  final String id;

  DetailsPage({required this.id});

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

通过以上步骤,我们完成了 go_router 的基本集成和简单使用。

2.2 go_router 的跳转与参数传递

最新版本中,go_router 的跳转和参数传递方法基本保持不变,但增加了一些新特性和改进。

  1. 基本跳转:

使用 context.go 进行页面跳转:

go 复制代码
context.go('/details/1');
  1. 通过路径参数传递数据:

在路由配置中使用路径参数,并在目标页面中获取:

less 复制代码
GoRoute(
  path: '/details/:id',
  builder: (context, state) => DetailsPage(id: state.params['id']!),
);
  1. 通过查询参数传递数据:

使用 queryParameters 传递查询参数:

go 复制代码
context.go('/details/1?name=John');

在目标页面中获取查询参数:

ini 复制代码
final name = state.queryParams['name'];
  1. 通过 extra 传递复杂数据:

使用 extra 传递复杂对象:

css 复制代码
context.go('/details/1', extra: {'key': 'value'});

在目标页面中获取 extra:

ini 复制代码
final extra = state.extra as Map<String, dynamic>?;
final value = extra?['key'];

示例:

我们先定义一个对象,然后通过 extra 的方式传递

dart 复制代码
class Item {
  final int id;
  final String name;

  Item({required this.id, required this.name});

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
      };

  factory Item.fromJson(Map<String, dynamic> json) => Item(
        id: json['id'],
        name: json['name'],
      );
}

在 GoRouter 配置中,使用 extra 参数传递复杂数据:

less 复制代码
final GoRouter _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/details',
      builder: (context, state) {
        final item = state.extra as Item?;
        return DetailsPage(item: item!);
      },
    ),
  ],
);

在目标页面 DetailsPage 中,通过构造函数获取参数:

scala 复制代码
class DetailsPage extends StatelessWidget {
  final Item item;

  DetailsPage({required this.item});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details Page')),
      body: Center(
        child: Text('Item ID: ${item.id}, Name: ${item.name}'),
      ),
    );
  }
}

在需要导航到 DetailsPage 的地方,例如在 HomePage 中,通过 context.go 传递参数:

less 复制代码
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final item = Item(id: 1, name: 'John Doe');

    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/details', extra: item);
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

对象就飞的使用 extra 来传递吗?我就像用 URL 的方式传参可以吗?

我们当然也可以将复杂数据序列化为 JSON 字符串,然后通过 URL 编码传递。

less 复制代码
final GoRouter _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/details/:data',
      builder: (context, state) {
        final jsonString = Uri.decodeComponent(state.params['data']!);
        final item = Item.fromJson(json.decode(jsonString));
        return DetailsPage(item: item);
      },
    ),
  ],
);

在需要导航的地方:

less 复制代码
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final item = Item(id: 1, name: 'John Doe');
    final jsonString = json.encode(item.toJson());
    final encodedJsonString = Uri.encodeComponent(jsonString);

    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.go('/details/$encodedJsonString');
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

但是我还是推荐,使用 GoRouter 的 extra 参数传递复杂数据类型,如对象、JSON、List、Map 等。这种方式更简洁和直观。使用URL传递基本数据类型即可,否则可能会导致 URL 过长和不易读。

通过这两种方式,你可以在 Flutter 应用中灵活地传递复杂数据类型到目标页面。

2.3 go_router 的常用API介绍
  1. 路由信息获取
ini 复制代码
final router = GoRouter.of(context);
  1. 路由栈管理

无法直接获取,需要通过 NavigatorObserver 的监听方式自己管理路由栈。

  1. 路由跳转与替换
go 复制代码
context.go('/books');  //直接导航到指定路径(path),并替换当前的导航历史记录(不可返回)
context.push('/books');   //直接推入指定路径path),保留当前的导航历史记录(可返回)
context.pushReplacement('/books');
context.goNamed("setting");  //通过命名路由导航到指定路径(name),支持路径参数和查询参数(不可返回)
context.pushNamed('setting'); //通过命名路由导航到指定路径(name),支持路径参数和查询参数 (可返回)
context.pushReplacementNamed('/books'); 

这里需要强调说一下,注意 go push pushReplacement 的区别,至于后缀 named 这个简单,只是路由定义中的使用 path 还是 name 的区别。

假设有三个页面:HomePage、SettingsPage 和 ProfilePage。

在 HomePage 中使用 context.go('/settings'); 之后导航堆栈会如下:

[ /settings ]

用户无法返回到 HomePage 或其他页面。

而在 HomePage 中使用 context.push('/settings'); 之后导航堆栈会如下:

[ /home, /settings ]

这是标准的跳转。

当我们在 SettingsPage 中使用 context.pushReplacement('/profile'); 之后导航堆栈会如下:

[ /home, /profile ]

不会新加入一个栈,也比较好理解。

此时我们在 ProfilePage 中使用context.go('/settings'); 之后导航堆栈会如下:

[ /settings ]

这么看大家就能理解了,使用 context.go 进行导航会重置整个导航堆栈,适用于希望用户无法回到之前页面的场景。使用 context.pushReplacement 进行导航仅替换当前页面,保留之前的导航堆栈,适用于需要保留导航历史的场景。使用 context.push 则是正常的入栈,适合保留廍的导航堆栈。

  1. 路由返回与弹出
scss 复制代码
context.canPop();    // 检查是否可以弹出当前路由
context.pop();       // 弹出当前路由
while (context.canPop()) {
  context.pop();     // 持续弹出直到根路由
}

那如果我想返回到指定的目标页面可以吗?我想跨多个页面返回可以吗?我想用 popUntilRouteWithName 这个 API 可以吗?

不好意思没有,虽然我们通过 context.go 和 context.pushReplacement 和 context.push 可以控制页面栈的管理,但是我们并不能查询这些栈,我们只能通过 NavigatorObserver 的方式自己实现,具体如何实现可以参考我之前的文章 GetX 的路由封装。

2.4 go_router 的守卫与重定向

go_router 提供了更灵活的守卫和重定向功能。

路由守卫用于在导航到某个页面之前进行一些条件检查,确保用户有权限或满足条件才允许导航到该页面。比如用户未登录时,拦截导航到需要认证的页面,并重定向到登录页面。

less 复制代码
final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => LoginPage(),
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => ProfilePage(),
      redirect: (state) {
        if (!isLoggedIn) {
          // 如果用户未登录,重定向到登录页面
          return '/login';
        }
        return null; // 用户已登录,允许导航
      },
    ),
  ],
);

在这个例子中,如果用户未登录(isLoggedIn 为 false),当尝试导航到 /profile 路径时,会被重定向到 /login 页面。

重定向则是无条件地将一个路径导航到另一个路径。使用重定向可以管理路径的变化,比如如果路径发生改变,或者为了向后兼容旧的路径。

php 复制代码
final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => SettingsPage(),
    ),
  ],
  redirect: (state) {
    final String fullPath = state.subloc;
    if (fullPath == '/old-settings') {
      // 如果访问旧的设置路径,重定向到新的设置页面
      return '/settings';
    }
    return null; // 没有重定向
  },
);

在这个例子中,如果访问 /old-settings 路径,用户会被重定向到 /settings 页面。

都是基于 redirect 实现的功能,特别是守卫拦截是很实用的,比如我如果我想在全部的路由中都加入这个未登录拦截的守卫呢?

我们也不需要再每一个 GoRoute 中加入 redirect 的拦截,我们可以在全局添加一个即可。

less 复制代码
final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => LoginPage(),
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => ProfilePage(),
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => SettingsPage(),
    ),
  ],
  redirect: (GoRouterState state) {
    // 获取当前路径
    final String fullPath = state.subloc;

    // 如果用户未登录,并且当前路径不是 /login,则重定向到 /login
    if (!isLoggedIn && fullPath != '/login') {
      return '/login';
    }
    // 如果用户已登录,且当前路径是 /login,则重定向到主页
    if (isLoggedIn && fullPath == '/login') {
      return '/';
    }
    return null; // 没有重定向
  },
);

是不是很方便呢?

2.5 go_router 的过渡动画

为了提升用户体验,我们可以在页面切换时添加过渡动画。go_router 提供了 pageBuilder 参数来实现自定义过渡动画。

less 复制代码
inal router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const MyHomePage(),
    ),
    GoRoute(
      path: '/setting',
      name: '/setting',
      // builder: (context, state) => const SettingPage(),
      pageBuilder: (context, state) {
        return CustomTransitionPage(
          key: state.pageKey,
          child: const SettingPage(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            // 定义页面进入动画
            return SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: child,
            );
          },
        );
      },
    ),
  ],
);

常用的还有渐变的动画:

php 复制代码
    pageBuilder: (context, state) {
        return CustomTransitionPage(
          key: state.pageKey,
          child: HomePage(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            // 定义页面进入动画
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
      },

其中:

FadeTransition:定义了一个渐隐渐现的动画效果。

SlideTransition:定义了一个从右往左的滑动动画效果。

除此之外你还可以定义骚操作,比如旋转的动画,缩放的动画(PS:我实在想不到有什么应用场景)

scala 复制代码
class ZoomTransitionPage extends CustomTransitionPage<void> {
  ZoomTransitionPage({required Widget child})
      : super(
          child: child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return ScaleTransition(
              scale: animation,
              child: child,
            );
          },
        );
}

class RotateTransitionPage extends CustomTransitionPage<void> {
  RotateTransitionPage({required Widget child})
      : super(
          child: child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return RotationTransition(
              turns: animation,
              child: child,
            );
          },
        );
}

如果我想使用自定义的动画可以吗,而不是预设的 RotationTransition ZoomTransitionPage SlideTransition FadeTransition 这些动画?

当然可以:

scala 复制代码
class MyCustomPanAndRotateTransition extends StatelessWidget {
  final Animation<double> animation;
  final Widget child;

  MyCustomPanAndRotateTransition({required this.animation, required this.child});

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Transform(
          transform: Matrix4.identity()
            ..translate(50 * (1.0 - animation.value))
            ..rotateZ(animation.value * 0.5 * 3.14),
          child: Opacity(
            opacity: animation.value,
            child: child,
          ),
        );
      },
      child: child,
    );
  }
}

定义一个自定义的平移和旋转动画直接在 GoRouter 中使用即可:

php 复制代码
GoRoute(
  path: '/details',
  pageBuilder: (context, state) {
    return CustomTransitionPage(
      key: state.pageKey,
      child: DetailsPage(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return MyCustomPanAndRotateTransition(animation: animation, child: child);
      },
    );
  },
);

如果我想全局的设置一个统一的动画效果呢?好像也没有。

我们只能通过抽取方法的方式来相对简洁的实现。

less 复制代码
// 通用的 CustomTransitionPage 构造方法
Page<dynamic> buildPageWithSlideTransition(BuildContext context, GoRouterState state, Widget child) {
  return CustomTransitionPage(
    key: state.pageKey,
    child: child,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(1.0, 0.0),
          end: Offset.zero,
        ).animate(animation),
        child: child,
      );
    },
  );
}

final GoRouter _router = GoRouter(
  routes: <GoRoute>[
    GoRoute(
      path: '/',
      pageBuilder: (context, state) => buildPageWithSlideTransition(context, state, MyHomePage()),
    ),
    GoRoute(
      path: '/setting',
      name: 'setting',
      pageBuilder: (context, state) => buildPageWithSlideTransition(context, state, SettingPage()),
    ),
  ],
);

好吧,基础的功能就演示到这里,下面我们就看看 aoto_router 的用法吧,它会不会有什么不同呢?

三、 auto_router 的使用

auto_router 是一个强大的路由管理库,可以帮助我们简化 Flutter 应用中的路由管理。它提供了强大的API支持,能够方便地实现页面跳转、参数传递、路由守卫、重定向以及过渡动画等功能。

2.1 auto_router 的集成与简单使用

在使用 auto_route 之前,需要在 pubspec.yaml 文件中添加 auto_route 和 auto_route_generator 依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  auto_route: ^8.0.0

dev_dependencies:
  build_runner: ^2.0.0
  auto_route_generator: ^8.0.0

定义我们的路由配置

scala 复制代码
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

part 'app_router.gr.dart';

@AutoRouterConfig(replaceInRouteName: 'Screen,Route')
class AppRouter extends _$AppRouter {

  @override
  List<AutoRoute> get routes => [

  ];
}

这里我指定生成的路由信息类为 PageRouter 格式,例如 FirstRoute 。

这是简单的配置,我们可以自定义什么样的文件会转换为什么样的Router,例如 @AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')

我们需要把全部的页面加上 @RoutePage() 的注解,例如:

scala 复制代码
@RoutePage()
class DetailsScreen extends ConsumerStatefulWidget {
}

然后我们运行生成代码 dart run build_runner build 默认会在同文件夹下生成对应的路由文件,内部就有对应的 DetailsRoute 这个类,这就是这个页面对应的路由对象了。

然后我们就可以在路由配置中添加对应的页面路由对象:

scala 复制代码
@AutoRouterConfig(replaceInRouteName: 'Screen,Route')
class AppRouter extends _$AppRouter {

  @override
  List<AutoRoute> get routes => [
    AutoRoute(page: DetailsRoute.page, path: '/detail'),
  ];
}

最后也别忘记了再入口中配置路由:

scala 复制代码
class MyApp extends StatelessWidget {
  
  final _appRouter = AppRouter();

  @override
  Widget build(BuildContext context) {

    return MaterialApp.router(
      routerConfig: _appRouter.config(
        navigatorObservers: () => [
          LogedNavigatorObserver(),
        ],
      ),
    );

  }
}

如何使用呢?

csharp 复制代码
context.router.push(const DetailsRoute());
或者
context.router.navigateNamed('/detail')

当然了,AutoRoute 的主要优势之一就是使用类型安全的路由对象进行页面跳转,也推荐直接拿到对应页面的路由对象就行。

2.2 auto_router 的跳转API与参数传递
2.2.1 基本跳转

使用 push 进行页面跳转:

less 复制代码
context.router.push(DetailsRoute(id: 1));
2.2.2 通过路径参数传递数据

在路由配置中使用路径参数,并在目标页面中定义:

less 复制代码
@MaterialAutoRouter(
  routes: <AutoRoute>[
    AutoRoute(page: HomePage),
    AutoRoute(page: DetailsPage, path: '/details/:id'),
  ],
)
class $AppRouter {}

class DetailsPage extends StatelessWidget {
  final int id;

  DetailsPage({@PathParam() required this.id});

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

通过 @PathParam() 定义与接收参数,通过构造传递参数:

less 复制代码
context.router.push(DetailsRoute(id: 1));

我们也可以通过路径字符串直接跳转,对于基本数据类型,这种方式也是支持携带参数的:

arduino 复制代码
context.router.pushNamed('/details/1');

但是,如果需要携带对象参数,只能通过生成路由对象来操作,不支持路径跳转。

2.2.3 通过路径参数传递数据 通过构造函数传递复杂数据

定义数据类并通过构造函数传递:

scala 复制代码
class Item {
  final int id;
  final String name;

  Item({required this.id, required this.name});
}

class DetailsPage extends StatelessWidget {
  final Item item;

  DetailsPage({required this.item});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details Page')),
      body: Center(
        child: Text('Item ID: ${item.id}, Name: ${item.name}'),
      ),
    );
  }
}

当我们运行 build_runner 生成对应的路由之后,我们就可以直接传递

less 复制代码
@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(page: HomePage, initial: true),
    AutoRoute(page: SettingPage),
  ],
)
class $AppRouter {}

对于对象形式的参数传递,不支持通过路径的方式传递参数。我们可以不写 path 属性,只能在应用内跳转:

ini 复制代码
final item = Item(id: 1, name: 'John Doe');
context.router.push(DetailsRoute(item: item));

那么简单的跳转与带参数的跳转我们就介绍到这里,接下来看看其他的常用的API吧。

2.3 auto_router 的常用API介绍
  1. 路由信息获取
ini 复制代码
final router = context.router;
RouteData? routeData = context.router.currentChild;
RouteData? routeData = ontext.router.current

可以获取到路由对象和当前的页面路由对象,可以获取到对应的 name 等信息。

  1. 路由栈管理

在上面的 go_router 中我们是无法直接获取,需要通过 NavigatorObserver 的监听方式自己管理路由栈。

在 auto_router 中我们可以根据 context.router.stack; 的方式获取当前全部的路由栈。

那么我们就可以很方便的获取到目标页面是否已经存在在栈中,从而进行一些逻辑判断。

  1. 路由跳转与替换
less 复制代码
context.router.push(DetailsRoute(id: 1));  // push 总是将新路由推入堆栈,保留导航历史,方便返回。
context.router.pushNamed('/details/1');  // 支持通过路径的方式跳转
context.router.navigate(DetailsRoute(id: 1))  //navigate 会尝试在导航堆栈中找到目标路由,如果存在则返回到该路由,否则就像 push 一样添加新路由。
context.router.navigateNamed(DetailsRoute(id: 1))
context.router.replace(DetailsRoute(id: 1));  //replace 用新路由替换当前路由。
context.router.replaceNamed('/details/1'); 

示例:

假设有三个页面:HomePage、SettingsPage 和 ProfilePage。

使用 push 在 HomePage 中使用 context.router.push(SettingsRoute()); 之后导航堆栈会如下:

[ /home, /settings ]

标准的堆栈式跳转,保留历史记录,可返回到 HomePage。

使用 replace 在 HomePage 中使用 context.router.replace(SettingsRoute()); 之后导航堆栈会如下:

[ /settings ]

替换当前页面,不保留 HomePage 的历史记录。

使用 navigate 在 HomePage 中使用 context.router.navigate(SettingsRoute());:

如果 SettingsPage 已在导航堆栈中,则返回到它,而不重新添加。

如果不在堆栈中,则效果与 push 相同。

那么如果在 SettingsPage 中使用 context.router.replace(ProfileRoute()); 之后导航堆栈会如下:

[ /home, /profile ]

替换当前页面,不增加新的栈。

那么如果在 SettingsPage 中使用 context.router.navigate(HomeRouter()); 之后导航堆栈会如下:

[ /home ]

此时的 navigate 就不是 push 的效果而是 pop 的效果了。

通过这些对比,开发者可以更直观地理解 auto_route 中不同导航方法的特点,以便根据需求选择合适的跳转方式。

  1. 路由的返回与多级返回

在 auto_route 中,处理页面导航和路由返回是非常常见的操作。下面我们将详细介绍一些常用的路由返回方法,以及如何使用它们。

  1. pop

从堆栈中弹出当前页面,并导航到前一个页面:

ini 复制代码
context.router.pop();
  1. maybePop

尝试从堆栈中弹出当前页面,如果堆栈中只有一个页面,则不会执行任何操作。

ini 复制代码
context.router.maybePop();

当你不确定当前页面是否是堆栈中的最后一个页面时,可以使用 maybePop 来避免程序崩溃或导航异常。

  1. popUntilRoot

弹出堆栈中的所有页面,直到返回到根页面。

ini 复制代码
context.router.popUntilRoot();

当你需要从子页面快速返回到根页面(通常是应用的首页)时,该方法非常有用。

  1. popUntilRouteWithName

弹出堆栈中的页面,直到找到指定名称的路由。

arduino 复制代码
context.router.popUntilRouteWithName('HomeRoute');

当你需要返回到某个特定的页面,而这个页面并不是根页面时,可以使用 popUntilRouteWithName 来精确控制导航行为。

  1. popUntil

根据条件弹出堆栈中的页面,直到满足条件。

ini 复制代码
context.router.popUntil((route) => route.settings.name == 'HomeRoute')

当你需要基于自定义条件返回到特定页面时,可以使用 popUntil 可以自定义条件而不是我这里用的 name 做判断,如果只是用 name 做条件可以用 popUntilRouteWithName 的方式。

  1. pushAndRemoveUntil

将新页面推入堆栈,并弹出所有不满足条件的页面。

ini 复制代码
context.router.pushAndRemoveUntil(
  ProfileRoute(),
  (route) => route.settings.name == 'HomeRoute'
);

当你需要跳转到一个新页面并清理堆栈中多余页面时,可以使用 pushAndRemoveUntil。

  1. removeLast

从堆栈中移除最后一个页面,但不会导航到前一个页面。

ini 复制代码
context.router.removeLast();

当你需要移除最后一个页面,但不想立即导航到其他页面时,可以使用 removeLast 的方式。

通过了解和使用这些 API 方法,你可以更灵活地控制应用的导航和返回行为,提高用户体验。

2.4 auto_router 的守卫与重定向

在 auto_route 中,我们可以通过 RouteGuard 实现导航守卫功能,以在导航到某个页面之前进行条件检查,比如用户是否已登录。如果不满足条件,可以重定向到其他页面,例如登录页面。

scala 复制代码
import 'package:auto_route/auto_route.dart';

class AuthGuard extends AutoRouteGuard {
  @override
  void onNavigation(NavigationResolver resolver, StackRouter router) {
    if (!isLoggedIn) {
      // 如果用户未登录,重定向到登录页面
      router.push(LoginRoute());
    } else {
      // 允许导航
      resolver.next(true);
    }
  }
}

在你的路由设置中,使用 guards 参数:

less 复制代码
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
class AppRouter extends _$AppRouter {
  @override
  List<AutoRoute> get routes => [
        AutoRoute(page: MyHomePageRoute.page, path: '/', initial: true),
        AutoRoute(page: SettingPageRoute.page, path: '/setting/:id', guards: [AuthGuard()]),
      ];
}

这里和 go_router 不同的是,它没有全局拦截的配置,我们为了方法可以抽取方法进行配置:

javascript 复制代码
AutoRoute guardedRoute({required PageRouteInfo Function() page, required String path}) {
  return AutoRoute(page: page, path: path, guards: [AuthGuard()]);
}

然后再需要拦截的地方设置守卫:

less 复制代码
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
class AppRouter extends _$AppRouter {
  @override
  List<AutoRoute> get routes => [
        guardedRoute(page: MyHomePageRoute.page, path: '/'),
        guardedRoute(page: SettingPageRoute.page, path: '/setting/:id'),
      ];
}

这种方式可以减少手动配置的重复性,并提高代码的可维护性。

2.5 auto_router 的过渡动画

默认实现的 AutoRoute 使用平台自带的过渡动画,我们如果想自定义页面的动画,我们可以使用 CustomRoute 对象,并且配置对应的 transitionsBuilder 对象。

php 复制代码
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
class AppRouter extends _$AppRouter {
  
  Widget fadeInTransition(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  }

  @override
  List<AutoRoute> get routes => [
        CustomRoute(
          page: MyHomePageRoute.page,
          path: '/',
          initial: true,
          transitionsBuilder: fadeInTransition,
        ),
        CustomRoute(
          page: SettingPageRoute.page,
          path: '/setting/:id',
          transitionsBuilder: fadeInTransition,
        ),
      ];
}

那么如果我想实现从右往左平移并且淡入淡出的动画,可以结合使用 SlideTransition 和 FadeTransition。

总结

我们简单的回顾了 go_routerauto_route 的简单与常用的用法,可以看出 go_router 相对比较而言轻量与简洁,适合中小项目,而 auto_route 的功能更全面 API 更丰富,更适合大型项目。

选择哪一个路由库更多取决于个人偏好和项目需求。如果项目中对类型安全和代码生成有偏好,并且愿意为此承担额外的学习成本,auto_route 可能是更好的选择。而如果你优先考虑易用性和快速上手,go_router 可能更适合。

最近我的项目可能就考虑使用 Riverpod 的注解生成,那么我选择 auto_route 也是顺便的事情,反正都是生成代码。个人也比较喜欢 auto_route 的跳转与返回等 API 感觉更加的灵活方便。

那本文的代码比较简单,相对比较基础,并且全部的代码也已经在文中展出,这里就不放出项目链接了,后期会直接给出 Riverpod + autoRouter 的项目 Dmeo 吧。

如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,理解不正确的地方,同学们都可以指出修正。

今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式推荐,也希望大家能评论区交流一起学习进步。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

相关推荐
谢小飞32 分钟前
我做了三把椅子原来纹理这样加载切换
前端·three.js
圈圈的熊32 分钟前
HTTP 和 HTTPS 的区别
前端·网络协议·http·https
GIS程序媛—椰子40 分钟前
【Vue 全家桶】2、Vue 组件化编程
前端·javascript·vue.js
Beamon__43 分钟前
element-plus按需引入报错IconsResolver is not a function
前端
努力奔波的程序猿44 分钟前
HBuilderx修改主题色-改变编辑器背景颜色等
前端
正小安1 小时前
Vue 3 性能提升与 Vue 2 的比较 - 2024最新版前端秋招面试短期突击面试题【100道】
前端·vue.js·面试
yqcoder1 小时前
electron 中 ipcRenderer 的常用方法有哪些?
前端·javascript·electron
T0uken1 小时前
【Python】Bottle:轻量Web框架
开发语言·前端·python
俎树振1 小时前
树莓派上安装与配置 Nginx Web 服务器教程
服务器·前端·nginx