【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,这一期就此完结。

相关推荐
桂月二二29 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者4 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794484 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存