前言
- Flutter中企業(yè)級路由
FluroRouter
的使用和原理分析 - 一些設(shè)計模式和思想
目錄
路由方式
原生跳轉(zhuǎn)
匿名方式
//Material方式循狰,動畫效果為從下到上漸變彈出頁面
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return DoorbellSettingScreen(deviceId);
}),
);
//IOS風(fēng)格,動畫效果為從右到左彈出頁面
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篓跛、跳轉(zhuǎn)
router.navigateTo(context, "/users/1234", transition: TransitionType.fadeIn);
對比
原生方式
- 模塊之間相互耦合膝捞,缺乏層次性
- 跳轉(zhuǎn)入口分散,統(tǒng)一攔截困難
- 調(diào)用方要知道跳轉(zhuǎn)class信息愧沟,協(xié)作困難
- 使用簡單蔬咬、代碼中可以直接跳轉(zhuǎn),閱讀方便
FluroRouter
- 模塊解耦沐寺,通過url方式跳轉(zhuǎn)
- 統(tǒng)一管理林艘,方便添加攔截器
- 不需要知道跳轉(zhuǎn)類信息,可預(yù)寫邏輯混坞,并行提高效率
- 可以通過
onGenerateRoute
做一些全局的路由跳轉(zhuǎn)前置處理邏輯 - 對于不熟悉路由規(guī)則的新同事狐援,跟蹤代碼吃力
結(jié)合項目
UML類圖
其中Routers是一個單例類,不同模塊相機究孕、門鈴等路由頁面在這里注冊啥酱,這里用了依賴倒置原則(DIP),用add方法依賴注入(DI)的方式實現(xiàn)了控制翻轉(zhuǎn)(IOC)厨诸,使Routers和具體的模塊路由頁面解耦镶殷,方便擴展
依賴倒置
設(shè)計模式的六大原則有:
Single Responsibility Principle:單一職責(zé)原則
Open Closed Principle:開閉原則
Liskov Substitution Principle:里氏替換原則
Law of Demeter:迪米特法則
Interface Segregation Principle:接口隔離原則
Dependence Inversion Principle:依賴倒置原則
把這六個原則的首字母聯(lián)合起來(兩個 L 算做一個)就是 SOLID (solid,穩(wěn)定的)泳猬,其代表的含義就是這六個原則結(jié)合使用的好處:建立穩(wěn)定批钠、靈活宇植、健壯的設(shè)計。
依賴倒置原則定義:
1埋心、上層模塊不應(yīng)該依賴底層模塊指郁,它們都應(yīng)該依賴于抽象。
2拷呆、抽象不應(yīng)該依賴于細(xì)節(jié)闲坎,細(xì)節(jié)應(yīng)該依賴于抽象。
pizza的例子:
倒置以后:
圖一的依賴箭頭都是從上往下的茬斧,圖二的箭頭出現(xiàn)了從下往上腰懂,依賴關(guān)系確實“倒置”了
另外,此例子也很好的解釋了“上層模塊不應(yīng)該依賴底層模塊项秉,它們都應(yīng)該依賴于抽象绣溜。”娄蔼,在最開始的設(shè)計中怖喻,高層模塊PizzaStroe直接依賴低層模塊(各種具體的Pizaa),調(diào)整設(shè)計后,高層模塊和低層模塊都依賴于抽象(Pizza)
依賴倒置岁诉、依賴注入的關(guān)系:
1.控制反轉(zhuǎn)是設(shè)計模式锚沸,遵從了依賴倒置原則
2.依賴注入是實現(xiàn)控制反轉(zhuǎn)的手段
策略模式
策略模式介紹
在軟件開發(fā)中常常遇到這樣的情況:實現(xiàn)一個功能可以有多種算法或策略,我們根據(jù)實際情況選擇不同的算法或策略完成該功能涕癣。例如哗蜈,排序算法,可以使用冒泡排序坠韩、快排距潘、歸并排序等等
針對這種情況,一種常規(guī)的方法是將多種算法寫在一個類中同眯。例如绽昼,需要提供多種排序算法唯鸭,可以將這些算法寫到一個類中须蜗,每一個方法對應(yīng)一個具體的排序算法;當(dāng)然目溉,也可以將這些排序算法封裝在一個統(tǒng)一的方法中明肮,通過if..else..或者case等條件判斷語句來選擇具體的算法。這兩種實現(xiàn)方式我們都可以稱為硬編碼缭付。然而柿估,當(dāng)很多個算法集中在一個類時,這個類就會變的臃腫陷猫,這個類的維護(hù)成本會變高秫舌,在維護(hù)時也更容易引發(fā)錯誤的妖。如果我們需要增加一種新的排序算法,需要修改封裝算法類的源代碼足陨。這就很明顯違反了OCP原則和單一職責(zé)原則
如果將這些算法或者策略抽象出來嫂粟,提供一個統(tǒng)一的接口,不同的算法或者策略有不同的實現(xiàn)類墨缘,這樣在程序客戶端就可以通過注入不同的實現(xiàn)對象來實現(xiàn)算法或者策略的動態(tài)替換星虹,這種模式的可擴展性、可維護(hù)性也就更高镊讼,也就是我們本章要說的策略模式
——《Android源碼設(shè)計模式》
策略模式UML:
Android 源碼中使用:
插值器
TimeInterpolator
宽涌、AccelerateDecelerateInterpolator
、DecelerateInterpolator
蝶棋、BounceInterpolator
估值器
TypeEvaluator
卸亮、ArgbEvaluator
、FloatEvaluator
FluroRouter使用前后對比圖:
可以看出路由表解除了不同模塊之間的相互依賴玩裙,為項目組件化設(shè)計提供支持嫡良,具體組件化的好處這里不再介紹
源碼分析
關(guān)鍵類
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);
// 節(jié)點名
String part;
In
//節(jié)點類型
RouteTreeNodeType type;
//路由(要跳轉(zhuǎn)的頁面,只有子節(jié)點有值)
List<AppRoute> routes = <AppRoute>[];
//子節(jié)點們
List<RouteTreeNode> nodes = <RouteTreeNode>[];
//父親節(jié)點
RouteTreeNode parent;
//是否是參數(shù)節(jié)點
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++) {
//遍歷處理路徑寝受,拿到當(dāng)前路徑值
String component = pathComponents[i];
//判斷當(dāng)前值是否已創(chuàng)建過節(jié)點,如果有復(fù)用
RouteTreeNode node = _nodeForComponent(component, parent);
if (node == null) {
//沒有創(chuàng)建過節(jié)點罕偎,按類型創(chuàng)建
RouteTreeNodeType type = _typeForComponent(component);
node = RouteTreeNode(component, type);
//設(shè)置當(dāng)前節(jié)點的父親節(jié)點
node.parent = parent;
if (parent == null) {
//如果當(dāng)前父親節(jié)點是null很澄,說明是一顆新樹,添加的總的_nodes樹集合中
_nodes.add(node);
} else {
//添加到父親節(jié)點的nodes中颜及,添加到樹上
parent.nodes.add(node);
}
}
//當(dāng)前值是路徑當(dāng)中最后一個值時甩苛,再去添加路由(只有葉子節(jié)點,有路由)
if (i == pathComponents.length - 1) {
if (node.routes == null) {
node.routes = [route];
} else {
node.routes.add(route);
}
}
//當(dāng)前節(jié)點當(dāng)作父親俏站,循環(huán)處理下一節(jié)點
parent = node;
}
}
_nodeForComponent中
判斷了當(dāng)前值節(jié)點是否已經(jīng)創(chuàng)建讯蒲,看下源碼
RouteTreeNode _nodeForComponent(String component, RouteTreeNode parent) {
//首先將根rootNode集合賦值給當(dāng)前變量
List<RouteTreeNode> nodes = _nodes;
if (parent != null) {
//如果parent不為空,取parent的節(jié)點覆蓋肄扎,從父親節(jié)點下匹配
// search parent for sub-node matches
nodes = parent.nodes;
}
for (RouteTreeNode node in nodes) {
//遍歷當(dāng)前父親的所有子節(jié)點墨林,有和自己相同的返回
if (node.part == component) {
return node;
}
}
//沒找到,返回null
return null;
}
添加的邏輯主要是判斷了當(dāng)前路徑值犯祠,在路由表中存不存在旭等,不存在新建節(jié)點,并掛到對應(yīng)的樹叉上衡载,存在就繼續(xù)向下查找
在代碼中抓取了一段注冊過程debug截圖搔耕,如下
從中可以觀察出以下幾個信息:
1、
_nodes
是一個樹的集合痰娱,也就是我們的路由表2弃榨、
part
部分為樹節(jié)點值菩收,其中:dn
節(jié)點為參數(shù)節(jié)點RouteTreeNodeType.parameter
3、只有葉子節(jié)點有路由地址
更直觀表示:
上圖的部分注冊流程為:
1鲸睛、device_add : DeviceAddScreen()
2坛梁、product/camera/device/:dn/settings : CameraSettingScreen()
3、product/doorbell/device/:dn/settings : DoorBellSettingScreen()
查找路由
下面看下匹配流程
對于 path = /product/camera/device/:dn/settings
的一條索引腊凶,查找要跳轉(zhuǎn)的頁面
/// 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 {
//如果沒找到钧萍,并且設(shè)置了回調(diào)褐缠,返回默認(rèn)頁
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);
}
//工廠模式根據(jù)動畫類型創(chuàng)建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
和原生路由的創(chuàng)建风瘦,具體查找的邏輯在_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 = ["/"];
}
//記錄最終匹配的節(jié)點值
Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches =
<RouteTreeNode, RouteTreeNodeMatch>{};
//將要檢查的節(jié)點队魏,最開始從根root表_nodes匹配
List<RouteTreeNode> nodesToCheck = _nodes;
for (String checkComponent in components) {
//當(dāng)前節(jié)點的匹配值,用來提升效率
Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches =
<RouteTreeNode, RouteTreeNodeMatch>{};
//下一路徑值要匹配的節(jié)點
List<RouteTreeNode> nextNodes = <RouteTreeNode>[];
String pathPart = checkComponent;
Map<String, List<String>> queryMap;
//處理在路徑上拼接的參數(shù)
if (checkComponent.contains("?")) {
var splitParam = checkComponent.split("?");
pathPart = splitParam[0];
queryMap = parseQueryString(splitParam[1]);
}
//遍歷要匹配的節(jié)點集合
for (RouteTreeNode node in nodesToCheck) {
//路徑值相等或者當(dāng)前是參數(shù)節(jié)點万搔,匹配成功
bool isMatch = (node.part == pathPart || node.isParameter());
if (isMatch) {
//處理參數(shù)
RouteTreeNodeMatch parentMatch = nodeMatches[node.parent];
RouteTreeNodeMatch match =
RouteTreeNodeMatch.fromMatch(parentMatch, node);
if (node.isParameter()) {
//處理:dn的參數(shù)paramKey = dn胡桨,值等于路徑值
String paramKey = node.part.substring(1);
match.parameters[paramKey] = [pathPart];
}
//處理?拼接的參數(shù)
if (queryMap != null) {
match.parameters.addAll(queryMap);
}
// print("matched: ${node.part}, isParam: ${node.isParameter()}, params: ${match.parameters}");
//記錄當(dāng)前匹配值
currentMatches[node] = match;
//記錄下一次要匹配的節(jié)點集合
if (node.nodes != null) {
nextNodes.addAll(node.nodes);
}
}
}
nodeMatches = currentMatches;
nodesToCheck = nextNodes;
//如果當(dāng)前路徑值沒有匹配上,說明沒注冊過瞬雹,退出
if (currentMatches.values.length == 0) {
return null;
}
}
//當(dāng)前nodeMatches的值為最后一個節(jié)點的值昧谊,把包含的路由信息和參數(shù)包裹成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;
}
以上就是路由的查找邏輯
總結(jié)
- 基于
FluroRouter
的路由可以更高效的管理頁面,并將模塊解耦酗捌,方便后續(xù)擴展 - 約定好路由規(guī)則后呢诬,可以提前寫好跳轉(zhuǎn)邏輯,并行開發(fā)
- 樹型結(jié)構(gòu)方便擴展攔截器功能
- 大量路由頁面時胖缤,共享樹干尚镰,節(jié)省內(nèi)存
-
printTree
方便查看頁面間關(guān)系 -
url
上拼接參數(shù)方式,傳遞簡單 - 后期可擴展注解聲明方式哪廓,類似后臺
controller
- 模塊內(nèi)使用匿名方式狗唉,跨模塊使用FluroRouter
參考鏈接
https://pub.dev/packages/fluro
https://flutter.dev/docs/development/ui/navigation