Flutter路由框架FluroRouter使用与源码分析

前言

  • Flutter中企业级路由FluroRouter的使用和原理分析
  • 一些设计模式和思想

目录

image.png

路由方式

原生跳转

匿名方式

//Material方式,动画效果为从下到上渐变弹出页面
Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) {
                    return DoorbellSettingScreen(deviceId);
                  }),
                );
//IOS风格,动画效果为从右到左弹出页面
Navigator.of(context).push(
            CupertinoPageRoute(builder: (context){
              return DoorbellSettingScreen();
            })
          );

FluroRouter

引入

1、Depend on it

Add this to your package's pubspec.yaml file:

dependencies:
  fluro: ^2.0.3

2、Install it

You can install packages from the command line:

$ flutter pub get

3、Import it

Now in your Dart code, you can use:

import 'package:fluro/fluro.dart';

使用

1、初始化

final router = FluroRouter();

2、注册

var usersHandler = Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
  return UsersScreen(params["id"][0]);
});

void defineRoutes(FluroRouter router) {
  router.define("/users/:id", handler: usersHandler);
  
  // it is also possible to define the route transition to use
  // router.define("users/:id", handler: usersHandler, transitionType: TransitionType.inFromLeft);
}

3、跳转

router.navigateTo(context, "/users/1234", transition: TransitionType.fadeIn);

对比

原生方式

  • 模块之间相互耦合,缺乏层次性
  • 跳转入口分散,统一拦截困难
  • 调用方要知道跳转class信息,协作困难
  • 使用简单、代码中可以直接跳转,阅读方便

FluroRouter

  • 模块解耦,通过url方式跳转
  • 统一管理,方便添加拦截器
  • 不需要知道跳转类信息,可预写逻辑,并行提高效率
  • 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑
  • 对于不熟悉路由规则的新同事,跟踪代码吃力

结合项目

UML类图

UML

其中Routers是一个单例类,不同模块相机、门铃等路由页面在这里注册,这里用了依赖倒置原则(DIP),用add方法依赖注入(DI)的方式实现了控制翻转(IOC),使Routers和具体的模块路由页面解耦,方便扩展

依赖倒置

设计模式的六大原则有:

  • Single Responsibility Principle:单一职责原则

  • Open Closed Principle:开闭原则

  • Liskov Substitution Principle:里氏替换原则

  • Law of Demeter:迪米特法则

  • Interface Segregation Principle:接口隔离原则

  • Dependence Inversion Principle:依赖倒置原则

把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。

依赖倒置原则定义:

1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。

pizza的例子:

pizza-1

倒置以后:

pizza-2

图一的依赖箭头都是从上往下的,图二的箭头出现了从下往上,依赖关系确实“倒置”了

另外,此例子也很好的解释了“上层模块不应该依赖底层模块,它们都应该依赖于抽象。”,在最开始的设计中,高层模块PizzaStroe直接依赖低层模块(各种具体的Pizaa),调整设计后,高层模块和低层模块都依赖于抽象(Pizza)

依赖倒置、依赖注入的关系:

image.png

1.控制反转是设计模式,遵从了依赖倒置原则
2.依赖注入是实现控制反转的手段

示例代码

策略模式

策略模式介绍

在软件开发中常常遇到这样的情况:实现一个功能可以有多种算法或策略,我们根据实际情况选择不同的算法或策略完成该功能。例如,排序算法,可以使用冒泡排序、快排、归并排序等等

针对这种情况,一种常规的方法是将多种算法写在一个类中。例如,需要提供多种排序算法,可以将这些算法写到一个类中,每一个方法对应一个具体的排序算法;当然,也可以将这些排序算法封装在一个统一的方法中,通过if..else..或者case等条件判断语句来选择具体的算法。这两种实现方式我们都可以称为硬编码。然而,当很多个算法集中在一个类时,这个类就会变的臃肿,这个类的维护成本会变高,在维护时也更容易引发错误。如果我们需要增加一种新的排序算法,需要修改封装算法类的源代码。这就很明显违反了OCP原则和单一职责原则

如果将这些算法或者策略抽象出来,提供一个统一的接口,不同的算法或者策略有不同的实现类,这样在程序客户端就可以通过注入不同的实现对象来实现算法或者策略的动态替换,这种模式的可扩展性、可维护性也就更高,也就是我们本章要说的策略模式

                                                                                                                                                          ——《Android源码设计模式》

策略模式UML:

策略模式

Android 源码中使用:

插值器

TimeInterpolatorAccelerateDecelerateInterpolatorDecelerateInterpolatorBounceInterpolator

估值器

TypeEvaluatorArgbEvaluatorFloatEvaluator

FluroRouter使用前后对比图:

路由模型

可以看出路由表解除了不同模块之间的相互依赖,为项目组件化设计提供支持,具体组件化的好处这里不再介绍

源码分析

关键类

RouteTree

/// A [RouteTree]
class RouteTree {
  // private
  final List<RouteTreeNode> _nodes = <RouteTreeNode>[];
  // addRoute - add a route to the route tree
  void addRoute(AppRoute route) {
  ... //省略部分代码
  }
  AppRouteMatch matchRoute(String path) {
    ... //省略部分代码
  }
}

RouteTreeNode

/// A node on [RouteTree]
class RouteTreeNode {
  // constructors
  RouteTreeNode(this.part, this.type);

  // 节点名
  String part;
  In
  //节点类型
  RouteTreeNodeType type;
  //路由(要跳转的页面,只有子节点有值)
  List<AppRoute> routes = <AppRoute>[];
  //子节点们
  List<RouteTreeNode> nodes = <RouteTreeNode>[];
  //父亲节点
  RouteTreeNode parent;
    //是否是参数节点
  bool isParameter() {
    return type == RouteTreeNodeType.parameter;
  }
}

添加路由

例如,注册一个:product/camera/device/:dn/settings : CameraSettingScreen()

// addRoute - add a route to the route tree
  void addRoute(AppRoute route) {
    var path = route.route;
    ... //省略部分代码
    List<String> pathComponents = path.split('/');
    RouteTreeNode parent;
    for (int i = 0; i < pathComponents.length; i++) {
      //遍历处理路径,拿到当前路径值
      String component = pathComponents[i];
      //判断当前值是否已创建过节点,如果有复用
      RouteTreeNode node = _nodeForComponent(component, parent);
      if (node == null) {
        //没有创建过节点,按类型创建
        RouteTreeNodeType type = _typeForComponent(component);
        node = RouteTreeNode(component, type);
        //设置当前节点的父亲节点
        node.parent = parent;
        if (parent == null) {
          //如果当前父亲节点是null,说明是一颗新树,添加的总的_nodes树集合中
          _nodes.add(node);
        } else {
          //添加到父亲节点的nodes中,添加到树上
          parent.nodes.add(node);
        }
      }
     //当前值是路径当中最后一个值时,再去添加路由(只有叶子节点,有路由)
      if (i == pathComponents.length - 1) {
        if (node.routes == null) {
          node.routes = [route];
        } else {
          node.routes.add(route);
        }
      }
      //当前节点当作父亲,循环处理下一节点
      parent = node;
    }
  }

_nodeForComponent中判断了当前值节点是否已经创建,看下源码

RouteTreeNode _nodeForComponent(String component, RouteTreeNode parent) {
    //首先将根rootNode集合赋值给当前变量
    List<RouteTreeNode> nodes = _nodes;
    if (parent != null) {
      //如果parent不为空,取parent的节点覆盖,从父亲节点下匹配
      // search parent for sub-node matches
      nodes = parent.nodes;
    }
    for (RouteTreeNode node in nodes) {
      //遍历当前父亲的所有子节点,有和自己相同的返回
      if (node.part == component) {
        return node;
      }
    }
    //没找到,返回null
    return null;
  }

添加的逻辑主要是判断了当前路径值,在路由表中存不存在,不存在新建节点,并挂到对应的树叉上,存在就继续向下查找

在代码中抓取了一段注册过程debug截图,如下

debug

从中可以观察出以下几个信息:

1、_nodes是一个树的集合,也就是我们的路由表

2、part部分为树节点值,其中:dn节点为参数节点RouteTreeNodeType.parameter

3、只有叶子节点有路由地址

更直观表示:

Tree

上图的部分注册流程为:

1、device_add : DeviceAddScreen()

2、product/camera/device/:dn/settings : CameraSettingScreen()

3、product/doorbell/device/:dn/settings : DoorBellSettingScreen()

查找路由

下面看下匹配流程

对于 path = /product/camera/device/:dn/settings 的一条索引,查找要跳转的页面


/// Similar to [Navigator.push] but with a few extra features.
  Future navigateTo(BuildContext context, String path,
      {bool replace = false,
      bool clearStack = false,
      bool maintainState = true,
      bool rootNavigator = false,
      bool nullOk = false,
      RoutePredicate predicate,
      TransitionType transition,
      Duration transitionDuration,
      RouteTransitionsBuilder transitionBuilder,
      RouteSettings routeSettings}) {
    //从注册表查找,返回一个routeMatch
    final routeMatch = matchRoute(context, path,
        transitionType: transition,
        transitionsBuilder: transitionBuilder,
        transitionDuration: transitionDuration,
        maintainState: maintainState,
        routeSettings: routeSettings);
    var route = routeMatch.route;
    final completer = Completer<String>();
    Future future = completer.future;
    if (routeMatch.matchType == RouteMatchType.nonVisual) {
      //如果是不可见的Widget,返回不可见
      completer.complete("Non visual route type.");
    } else {
      //如果没找到,并且设置了回调,返回默认页
      if (route == null && notFoundHandler != null) {
        route = _notFoundRoute(context, path, maintainState: maintainState);
      }
      if (route != null) {
        final navigator =
            Navigator.of(context, rootNavigator: rootNavigator, nullOk: nullOk);
        if (clearStack) {
          //是否是清栈
          future = navigator.pushAndRemoveUntil(
              route, predicate ?? (check) => false);
        } else {
          //是否是替换
          future = replace
              ? navigator.pushReplacement(route)
              : navigator.push(route);
        }
        completer.complete();
      } else {
        final error = "No registered route was found to handle '$path'.";
        print(error);
        completer.completeError(RouteNotFoundException(error, path));
      }
    }

    return future;
  }

/// Attempt to match a route to the provided [path].
  RouteMatch matchRoute(BuildContext buildContext, String path,
      {RouteSettings routeSettings,
      TransitionType transitionType,
      Duration transitionDuration,
      RouteTransitionsBuilder transitionsBuilder,
      bool maintainState = true}) {
    var settingsToUse = routeSettings;
    ... //省略部分
    final match = _routeTree.matchRoute(path);
    final route = match?.route;
        ... //省略
    // 没有找到,返回
    if (route == null && notFoundHandler == null) {
      return RouteMatch(
          matchType: RouteMatchType.noMatch,
          errorMessage: "No matching route was found");
    }
    final parameters =
        match?.parameters ?? <String, List<String>>{};
    //处理类型是否是screen
    if (handler.type == HandlerType.function) {
      handler.handlerFunc(buildContext, parameters);
      return RouteMatch(matchType: RouteMatchType.nonVisual);
    }

    //工厂模式根据动画类型创建PageRoute
    final creator =
        (RouteSettings routeSettings, Map<String, List<String>> parameters) {
      final isNativeTransition = transition == TransitionType.native ||
          transition == TransitionType.nativeModal;
      if (isNativeTransition) {
        return MaterialPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog: transition == TransitionType.nativeModal,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else if (transition == TransitionType.material ||
          transition == TransitionType.materialFullScreenDialog) {
        return MaterialPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog:
                transition == TransitionType.materialFullScreenDialog,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else if (transition == TransitionType.cupertino ||
          transition == TransitionType.cupertinoFullScreenDialog) {
        return CupertinoPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog:
                transition == TransitionType.cupertinoFullScreenDialog,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else {
        RouteTransitionsBuilder routeTransitionsBuilder;

        if (transition == TransitionType.custom) {
          routeTransitionsBuilder =
              transitionsBuilder ?? route?.transitionBuilder;
        } else {
          routeTransitionsBuilder = _standardTransitionsBuilder(transition);
        }

        return PageRouteBuilder<dynamic>(
          settings: routeSettings,
          maintainState: maintainState,
          pageBuilder: (BuildContext context, Animation<double> animation,
              Animation<double> secondaryAnimation) {
            return handler.handlerFunc(context, parameters);
          },
          transitionDuration: transition == TransitionType.none
              ? Duration.zero
              : transitionDuration ?? route?.transitionDuration,
          reverseTransitionDuration: transition == TransitionType.none
              ? Duration.zero
              : transitionDuration ?? route?.transitionDuration,
          transitionsBuilder: transition == TransitionType.none
              ? (_, __, ___, child) => child
              : routeTransitionsBuilder,
        );
      }
    };
    //返回匹配的可见类型的RouteMatch
    return RouteMatch(
      matchType: RouteMatchType.visual,
      route: creator(settingsToUse, parameters),
    );
  }

FlourRoutermatchRoute方法主要处理了返回是否可见的RouteMatch和原生路由的创建,具体查找的逻辑在_routeTree.matchRoute(path)

matchRoute

AppRouteMatch matchRoute(String path) {
    String usePath = path;
    if (usePath.startsWith("/")) {
      usePath = path.substring(1);
    }
    //按路径分割字符串
    List<String> components = usePath.split("/");
    if (path == Navigator.defaultRouteName) {
      components = ["/"];
    }
        //记录最终匹配的节点值
    Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches =
        <RouteTreeNode, RouteTreeNodeMatch>{};
    //将要检查的节点,最开始从根root表_nodes匹配
    List<RouteTreeNode> nodesToCheck = _nodes;
    for (String checkComponent in components) {
      //当前节点的匹配值,用来提升效率
      Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches =
          <RouteTreeNode, RouteTreeNodeMatch>{};
      //下一路径值要匹配的节点
      List<RouteTreeNode> nextNodes = <RouteTreeNode>[];
      String pathPart = checkComponent;
      Map<String, List<String>> queryMap;
      //处理在路径上拼接的参数
      if (checkComponent.contains("?")) {
        var splitParam = checkComponent.split("?");
        pathPart = splitParam[0];
        queryMap = parseQueryString(splitParam[1]);
      }
      //遍历要匹配的节点集合
      for (RouteTreeNode node in nodesToCheck) {
        //路径值相等或者当前是参数节点,匹配成功
        bool isMatch = (node.part == pathPart || node.isParameter());
        if (isMatch) {
          //处理参数
          RouteTreeNodeMatch parentMatch = nodeMatches[node.parent];
          RouteTreeNodeMatch match =
              RouteTreeNodeMatch.fromMatch(parentMatch, node);
          if (node.isParameter()) {
            //处理:dn的参数paramKey = dn,值等于路径值
            String paramKey = node.part.substring(1);
            match.parameters[paramKey] = [pathPart];
          }
          //处理?拼接的参数
          if (queryMap != null) {
            match.parameters.addAll(queryMap);
          }
//          print("matched: ${node.part}, isParam: ${node.isParameter()}, params: ${match.parameters}");
          //记录当前匹配值
          currentMatches[node] = match;
          //记录下一次要匹配的节点集合
          if (node.nodes != null) {
            nextNodes.addAll(node.nodes);
          }
        }
      }
      nodeMatches = currentMatches;
      nodesToCheck = nextNodes;
      //如果当前路径值没有匹配上,说明没注册过,退出
      if (currentMatches.values.length == 0) {
        return null;
      }
    }
    //当前nodeMatches的值为最后一个节点的值,把包含的路由信息和参数包裹成AppRouteMatch返回
    List<RouteTreeNodeMatch> matches = nodeMatches.values.toList();
    if (matches.length > 0) {
      RouteTreeNodeMatch match = matches.first;
      RouteTreeNode nodeToUse = match.node;
//          print("using match: ${match}, ${nodeToUse?.part}, ${match?.parameters}");
      if (nodeToUse != null &&
          nodeToUse.routes != null &&
          nodeToUse.routes.length > 0) {
        List<AppRoute> routes = nodeToUse.routes;
        AppRouteMatch routeMatch = AppRouteMatch(routes[0]);
        routeMatch.parameters = match.parameters;
        return routeMatch;
      }
    }
    return null;
  }

以上就是路由的查找逻辑

总结

  • 基于FluroRouter的路由可以更高效的管理页面,并将模块解耦,方便后续扩展
  • 约定好路由规则后,可以提前写好跳转逻辑,并行开发
  • 树型结构方便扩展拦截器功能
  • 大量路由页面时,共享树干,节省内存
  • printTree方便查看页面间关系
  • url上拼接参数方式,传递简单
  • 后期可扩展注解声明方式,类似后台controller
  • 模块内使用匿名方式,跨模块使用FluroRouter

参考链接

https://pub.dev/packages/fluro

https://flutter.dev/docs/development/ui/navigation

https://flutterchina.club/

https://www.jianshu.com/p/c3ce6762257c

https://blog.csdn.net/leila_W/article/details/90105192

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容