Flutter - 优雅的使用官方路由 go_router

3,848 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

笔者最近在对 App 项目 Flutter Web 化落地中。路由建设上,在 Web 上肯定无法使用 flutter_boost,那显然的遇到了一些路由的相关改造。

这里把一些笔者感觉更为优雅的实现介绍给大家。

背景

go_router 作为官方路由(推荐),应该是我们使用和学习的对象。

其他选型上这里就不一一列举,掘金上很多关于 Flutter 路由的科普文章,写的都挺好的 ~

这里可能有些同学分不清哪些插件是官方库还是三方库,这里笔者使用的方式是去看他 github 所在仓库位置,是否是在 github.com/flutter 官方仓库下

go_router 更新也很频繁,不到10天就会有一个新版本,毕竟是官方在维护,如果有经常关注 Flutter issues 的同学,就会发现在 go_router issues 下也有 darshankawar 这位老哥的身影 ~

另外说一句,当前 go_router 的版本是 5.2.0,但在 Flutter 3.0 ~ 3.3 之间最高只能用到 4.5.1。

go_router 问题也是有的,能查到的资料确实太少了,也是因为它正式出现还不到一年,大家可能还是比较少的选择它(很多同学会选择 GetX 全家桶,或者和笔者公司一样使用 flutter_boost 来做混合开发)。

下面就给大家介绍下笔者如何去解决遇到的一些问题。

问题

实现全局 context

go_router 中是需要 context 上下文来实现路由相关能力的。

但在 flutter_boost 的实现上并不需要 context (跨端的路由控制)。

所以在 Web 化的过程中,需要在不改变 App 实现方式的基础上,提供出一套不需要 contextgo_router

原理还是很简单,直接放实现代码,可以拿来直接用:

// ./routes/application.dart

/// 构造一个全局路由观察者
class GDNavigatorObserver extends NavigatorObserver {
  static GDNavigatorObserver instance = GDNavigatorObserver();
}

// ./main.dart
void main() {

  ...
      
  final GoRouter _router = GoRouter(
    observers: [GDNavigatorObserver.instance], // 挂载观察者
    routes: routes, // 路由定义
    errorBuilder: (context, state) => errorPage(), // 错误页面
    redirect: (state) => redirect(state), // 重定向页面
  );
}

使用上,即可在任意位置,通过如下代码获取到当前页面的 context

var context = GDNavigatorObserver.instance.navigator?.context; 

需要额外注意context 的生命周期,比如要是用了 showBottomSheet 之类的本页弹出,不应该使用上面的 context.pop(),这会导致整个页面返回,而应该使用 对于 Flutter 库中的基础路由 Navigator.of(context).pop()

增加 pop(result) 能力

对于 Flutter 库中的基础路由 Navigator.of(context),是有一种 B 页面 -> A 页面回传的方式的:

// A 页面

/// 会等待 B 页面回调的结果
var result = await Navigator.of(context).push(B);

// B 页面

/// 返回 A 页面,且可传任意类型的回调参数 result
Navigator.of(context).pop(result);

go_router 中却是不支持的,这在 issues 里也能找到具体说明: github.com/flutter/flu…

官方认为这可能是问题,但不急于去解决的(P5)。也可以看到快半年了,迭代了这么多个版本,并没有去解决。

那在笔者实际项目里,flutter_boost 也是提供了相应的 pop(result) 的能力,且项目中确实有页面使用到。那如何去实现这个能力呢?

这在 issues 中有位老哥提供了实现方式 github.com/MrOnyszko/p… 他是通过 fork go_router 进行侵入式改造,但官方不合他的代码肯定是有官方的考虑,那我们也不应该通过侵入的方式修改,毕竟这库更新这么那么的频繁 ...

那从具体实现上,封装一层方法出来来使用即可,关键代码如下:

// ./gaoding_native_bridge/router_provider.dart

// 路由封装
class RouterProvider extends IRouter {
  /// 记录完成时的回调 <location,Future>
  final Map<String, Completer<dynamic>> _completers =
      <String, Completer<dynamic>>{};

  @override
  // 封装 pop 方法
  Future<bool> pop({
    Map<String, dynamic> result = const {},
    ...
  }) async {
  // 获取当前的 context
  var context = GDNavigatorObserver.instance.navigator?.context;
    // 判断是否可以 pop
    if (context != null && context.canPop()) {
      // 拿到当前的路径,作为 key
      final path = GoRouter.of(context).location;
      final Completer<dynamic>? completer = _completers[path];
      if (completer != null) {
        // 发起回调,给上一页面传递 result
        completer.complete(result);
      }
      // 从 map 中移除
      _completers.remove(path);
      // 调用 GoRouter 的 pop
      context.pop();
    } 
    ...
    return true;
  }

  @override
  // 封装 push 方法
  Future push(...) async {
    // 获取当前的 context
    var context = GDNavigatorObserver.instance.navigator?.context;
    if (context == null) {
      return;
    }
    // GoRouter push 方法
    context.pushNamed(...);
    // 定义 Completer
    final Completer<dynamic> completer = Completer<dynamic>();
    // 塞入 map
    _completers[GoRouter.of(context).location] = completer;
    // 返回 future
    return completer.future;
  }
}

使用上,跟 Navigator 一致:

// A 页面
var result = await GDBridgeAPI.router.push();

// B 页面
GDBridgeAPI.router.pop(result);

GDBridgeAPI 的实现可以看看笔者的另一篇文章:Flutter Web - 优雅的兼容 Flutter App 代码

合理定义路由

这部分算是一个综合设计,在保持 App 上路由 URL 不变的前提下,扩展成 go_router 推荐的使用形式。

直接看代码:


// ./routes/define.dart

class RouterURL {
  /// 名称
  final String name;
  /// 路径
  final String path;

  const RouterURL({
    required this.name,
    required this.path,
  });

  /// 首页
  static RouterURL home = const RouterURL(
    name: 'home',
    path: '/',
  );
  ...
}

/// App URL 路由映射
final navigateToFlutterRoutes = {
  'gaoding://home': RouterURL.home,
  ...
};

// ./routes/routes.dart

final routes = [
  // 首页
  GoRoute(
    name: RouterURL.home.name,
    path: RouterURL.home.path,
    builder: (context, state) => const GDRootPage(),
  ),
  
  ...
];

简单示例,routes.dart 定义整个项目中 go_router 路由与页面的关系。 define.dart 定义各个常量,用于页面间使用。

context.push(context.namedLocation(
  RouterURL.home.name,
));

navigateToFlutterRoutes 用于关联 App 和 go_router 路由之间的关系。

总结

这里想谈谈如何去更好的熟悉使用开源库,毕竟去等待有人发相关的文章,那真的不如趁早使用资料多的[狗头]。

而对于这样资料少的开源库(一般只看官方库),笔者一般第一步也是从 github 中看它们的使用示例(examples)以及 issues 来寻找解决问题的方案。

再找不到解决思路,才会去看源码实现,毕竟官方库的代码设计也是值得学习的。

如果对你开发学习上有丝丝作用,请点个赞[开心] ~

(十分感谢评论老哥提的建议,补充了一些注释说明)