Flutter Navigator 1.0 路由封装源码分享

230 阅读5分钟

1. 路由是什么?

前端开发者其实都十分清楚这个问题的答案,通俗易懂来说,路由就是负责页面跳转,而路由管理则是控制盒管理页面跳转的过程和规则,而Flutter中的路由也分两个版本

  • Navigator 1.0:侧重于移动端的路由系统,采用命令式编程风格,通过push和pop方法管理路由栈。
  • Navigator 2.0:在Flutter 1.22版本后新增,侧重于复杂的桌面端/网页端,采用声明式编程风格,使用Router和RouterInformationParser等类来描述和管理路由树。

而本篇我是针对1.0版本进行一个封装,分享我在自己移动端的项目中使用得比较顺手的路由相关工具类或拓展类。

2. 静态工具类相关

2.1 路由静态管理类

我们使用路由,会很频繁的去调用Navigator.of(context),来获取路由的能力,然后有些时候我们没有context的时候,我们需要一个GlobalKey来获取路由能力,要写很多重复代码,所以我就简单封装了一下工具类来解决以上的问题

///项目路由工具类
///可通过该类的静态方法进行路由操作
///另外也对BuildContext进行了函数扩展,可以通过BuildContext对象进行对Nav的调用,详情查看NavExtension
class Nav {
  static final GlobalKey<NavigatorState> globalNavigatorKey =
      GlobalKey<NavigatorState>();

  ///通过Router对象跳转,一般情况下尽量不使用该方法,请通过路由名跳转
  static Future<T?> pushRouter<T extends Object?>(
      BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }

  ///通过路由名称跳转至新路由
  ///一般情况下尽量传Context
  ///返回值:通过Future接收页面数据返回对象
  static Future<T?> push<T extends Object?>(
      {BuildContext? context, required String routerName, Map<String,dynamic>? arguments}) {
    if (context == null) {
      return _getGlobalNaigatorState()
          .pushNamed(routerName, arguments: arguments);
    } else {
      FocusUtil.cancelFocus(context);
      return Navigator.of(context).pushNamed(routerName, arguments: arguments);
    }
  }

  ///通过路由名称跳转至新路由,并且结束当前页面
  ///一般情况下尽量传Context
  ///返回值:通过Future接收页面数据返回对象
  static Future<T?> pushReplacement<T extends Object?>(
      {BuildContext? context, required String routerName,  Map<String,dynamic>? arguments}) {
    if (context == null) {
      return _getGlobalNaigatorState()
          .pushReplacementNamed(routerName, arguments: arguments);
    } else {
      FocusUtil.cancelFocus(context);
      return Navigator.of(context)
          .pushReplacementNamed(routerName, arguments: arguments);
    }
  }

  ///通过路由名称跳转至新路由,并且批量结束页面,直到predicate返回true
  ///一般情况下尽量传Context
  ///返回值:通过Future接收页面数据返回对象
  static Future<T?> pushAndRemoveUntil<T extends Object?>(
      {BuildContext? context,
      required String routerName,
        Map<String,dynamic>? arguments,
      required RoutePredicate predicate}) {
    if (context == null) {
      return _getGlobalNaigatorState()
          .pushNamedAndRemoveUntil(routerName, predicate, arguments: arguments);
    } else {
      FocusUtil.cancelFocus(context);
      return Navigator.of(context)
          .pushNamedAndRemoveUntil(routerName, predicate, arguments: arguments);
    }
  }

  ///弹出当前页面,并且可以
  ///一般情况下尽量传Context
  static void pop<T extends Object?>({BuildContext? context, T? result}) {
    if (context == null) {
      _getGlobalNaigatorState().pop(result);
    } else {
      Navigator.pop(context, result);
    }
  }

  ///批量结束页面,直到predicate返回true
  ///一般情况下尽量传Context
  static void popUntil<T extends Object?>(
      {BuildContext? context, required RoutePredicate predicate}) {
    if (context == null) {
      _getGlobalNaigatorState().popUntil(predicate);
    } else {
      Navigator.popUntil(context, predicate);
    }
  }

  ///全局NaigatorState,在某些没Context的下,可通过该对象进行路由操作
  static NavigatorState _getGlobalNaigatorState() {
    return globalNavigatorKey.currentState!;
  }

  ///获取当前页面传参数据,也可通过BuildContext的getRouterArgument获得;
  static dynamic getRouterArgument(BuildContext context) {
    return ModalRoute.of(context)?.settings.arguments;
  }
}

该类在使用方式上跟原生调用是一致的,我就不举例说明怎么使用了,大家可以根据函数注释来理解方法。

2.1.1 工具类初始化

该工具类需要在程序入口MaterialApp类中,注册一下navigatorKey,这样就能不需要context就能调用路由能力了。 例:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
                  routes: routerMap,
                  //注册全局key获得能力
                  navigatorKey: Nav.globalNavigatorKey,
                );

  }

2.2 全局焦点管理类

我们有时候在跳转页面的时候,软键盘已经弹出,那么这个时候,我们一般软键盘会伴随页面跳转而顺便隐藏,大家可以看到上面的push方法中,利用了FocusUtil来隐藏了键盘

static Future<T?> push<T extends Object?>(
      {BuildContext? context, required String routerName, Map<String,dynamic>? arguments}) {
      //隐藏软键盘
      FocusUtil.cancelFocus(context);
      return Navigator.of(context).pushNamed(routerName, arguments: arguments);
  }

接着我也放一下这个软键盘管理类的源码

import 'package:flutter/material.dart';

class FocusUtil {
  static cancelFocus(BuildContext context) {
    if (hasFocus(context)) {
      FocusManager.instance.primaryFocus!.unfocus();
    }
  }
// 目前是否有
  static bool hasFocus(BuildContext context) {
    FocusScopeNode currentFocus = FocusScope.of(context);
    if (currentFocus.focusedChild != null && !currentFocus.hasPrimaryFocus) {
      return true;
    }
    return false;
  }
}

2.3 context路由拓展类

封装完后我发现,其实调用工具类的时候,我们都是依赖context作为跳转的,那么是不是我们可以利用Flutter的拓展特性,更加方便的调用呢?比如

context.push("page")

那么想到这里,我又为context进行了一次路由的拓展

///为BuildContext 拓展一些路由的函数,一一对应Nav中的函数
extension NavExtension on BuildContext {
  Future<T?> pushRouter<T extends Object?>(Route<T> route) {
    return Nav.pushRouter(this, route);
  }

  Future<T?> push<T extends Object?>(
      {required String routerName, Map<String, dynamic>? arguments}) {
    return Nav.push(
        context: this, routerName: routerName, arguments: arguments);
  }

  Future<T?> pushReplacement<T extends Object?>(
      {required String routerName, Map<String, dynamic>? arguments}) {
    return Nav.pushReplacement(
        context: this, routerName: routerName, arguments: arguments);
  }

  void pop<T extends Object?>({BuildContext? context, T? result}) {
    Nav.pop(context: this, result: result);
  }

  void popUntil<T extends Object?>(
      { required RoutePredicate predicate}) {
    Nav.popUntil(context: this, predicate: predicate);
  }

  Future<T?> pushAndRemoveUntil<T extends Object?>(
      {
      required String routerName,
      Map<String, dynamic>? arguments,
      required RoutePredicate predicate}) {
    return Nav.pushAndRemoveUntil(
        routerName: routerName,
        predicate: predicate,
        context: this,
        arguments: arguments);
  }

  Map<String, dynamic> getRouterArgument() {
    return Nav.getRouterArgument(this)??{};
  }
}

这样我们就为context对象赋能了路由的能力了~在日常开发里面真的非常方便!

同学们也可以为State或者StatlessWidget去拓展一下,方便自己调用。

那么核心的工具类源码分享就是以上的内容,希望可以帮到各位同学在使用Nav1.0中更加的得心应手。

3. 路由Map配置技巧

这里顺便说点题外话,我项目中是怎么设置路由的,大家从上面的代码可以看出来,我塞了一个Map给routers属性,我们看看这个Map是怎么定义的

  1. 首先我们是静态声明一个Map到MaterialApp中
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
                  routes: routerMap,
                );

  }
  1. 定义全局的routerMap,通过...把每个模块的独立Map配置解耦到总Map中
///全局路由配置
///1.新建模块时,需要在模块中新增${moduleName}_router.dart文件,在文件中声明一个名称为${moduleName}RoutetMap的Map,存放该模块的页面路由
///2.路由名称声明规范为:${moduleName}/${PageName}
///3.新建的模块Map,通过...map解包与routerMap中
final Map<String, WidgetBuilder> routerMap = {
  ...AModuleRouterMap,
  ...BModuleRouterMap,
  ...CMoudleRouterMap,
};
  1. 定义独立模块的路由地址与映射关系
class AModuleRouter {
  ///搜索客户
  static const searchPage = "/customer/searchPage";
  ///搜索客户列表
  static const searchListPage = "/customer/searchListPage";
  ///我的客户
  static const mineCustomerListPage = "/customer/mineCustomerListPage";
  ///客户详情
  static const detailPage = "/customer/detailPage";
}


final Map<String, WidgetBuilder> AMoudleRouterMap = {
  CustomerRouter.searchPage : (context) => CustomerSearchPage(),
  CustomerRouter.searchListPage : (context) => CustomerSearchListPage(),
  CustomerRouter.mineCustomerListPage : (context) => CustomerMineListPage(),
  CustomerRouter.detailPage : (context) => CustomerDetailPage(),
};

这样我们的路由结构定义就十分清晰了~同学们也可以参考这种配置的方式去使自己的路由配置更加清晰,没必要一股脑的往总路由Map中塞入。

那么本篇文章就到此结束了,觉得有帮助的朋友不妨点点赞和关注,这对我十分有帮助~谢谢大家!

其他Flutter相关文章: Flutter Mvvm实践 Flutter setState流程探索