前言
- Flutter中企业级路由
FluroRouter
的使用和原理分析 - 一些设计模式和思想
目录
路由方式
原生跳转
匿名方式
//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类图
其中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的例子:
倒置以后:
图一的依赖箭头都是从上往下的,图二的箭头出现了从下往上,依赖关系确实“倒置”了
另外,此例子也很好的解释了“上层模块不应该依赖底层模块,它们都应该依赖于抽象。”,在最开始的设计中,高层模块PizzaStroe直接依赖低层模块(各种具体的Pizaa),调整设计后,高层模块和低层模块都依赖于抽象(Pizza)
依赖倒置、依赖注入的关系:
1.控制反转是设计模式,遵从了依赖倒置原则
2.依赖注入是实现控制反转的手段
策略模式
策略模式介绍
在软件开发中常常遇到这样的情况:实现一个功能可以有多种算法或策略,我们根据实际情况选择不同的算法或策略完成该功能。例如,排序算法,可以使用冒泡排序、快排、归并排序等等
针对这种情况,一种常规的方法是将多种算法写在一个类中。例如,需要提供多种排序算法,可以将这些算法写到一个类中,每一个方法对应一个具体的排序算法;当然,也可以将这些排序算法封装在一个统一的方法中,通过if..else..或者case等条件判断语句来选择具体的算法。这两种实现方式我们都可以称为硬编码。然而,当很多个算法集中在一个类时,这个类就会变的臃肿,这个类的维护成本会变高,在维护时也更容易引发错误。如果我们需要增加一种新的排序算法,需要修改封装算法类的源代码。这就很明显违反了OCP原则和单一职责原则
如果将这些算法或者策略抽象出来,提供一个统一的接口,不同的算法或者策略有不同的实现类,这样在程序客户端就可以通过注入不同的实现对象来实现算法或者策略的动态替换,这种模式的可扩展性、可维护性也就更高,也就是我们本章要说的策略模式
——《Android源码设计模式》
策略模式UML:
Android 源码中使用:
插值器
TimeInterpolator
、AccelerateDecelerateInterpolator
、DecelerateInterpolator
、BounceInterpolator
估值器
TypeEvaluator
、ArgbEvaluator
、FloatEvaluator
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截图,如下
从中可以观察出以下几个信息:
1、
_nodes
是一个树的集合,也就是我们的路由表2、
part
部分为树节点值,其中:dn
节点为参数节点RouteTreeNodeType.parameter
3、只有叶子节点有路由地址
更直观表示:
上图的部分注册流程为:
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),
);
}
FlourRouter
的matchRoute
方法主要处理了返回是否可见的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