Jonatas

clean route parser implementation and add test to dynamic routes

  1 +## [3.25.0]
  2 +
1 ## [3.24.0] 3 ## [3.24.0]
2 - GetWidget has been completely redesigned. 4 - GetWidget has been completely redesigned.
3 Throughout its lifetime, GetWidget has always been mentioned in the documentation as "something you shouldn't use unless you're sure you need it", and it had a very small use case. A short time ago we realized that it could have some unexpected behaviors, when compared to GetView, so we decided to rebuild it from scratch, creating a really useful widget for the ecosystem. 5 Throughout its lifetime, GetWidget has always been mentioned in the documentation as "something you shouldn't use unless you're sure you need it", and it had a very small use case. A short time ago we realized that it could have some unexpected behaviors, when compared to GetView, so we decided to rebuild it from scratch, creating a really useful widget for the ecosystem.
  1 +export 'get_connect/connect.dart';
@@ -895,9 +895,7 @@ extension GetNavigation on GetInterface { @@ -895,9 +895,7 @@ extension GetNavigation on GetInterface {
895 routeTree = ParseRouteTree(); 895 routeTree = ParseRouteTree();
896 } 896 }
897 897
898 - for (final element in getPages) {  
899 - routeTree.addRoute(element);  
900 - } 898 + routeTree.addRoutes(getPages);
901 } 899 }
902 } 900 }
903 901
1 -import 'package:flutter/widgets.dart';  
2 import '../../get_navigation.dart'; 1 import '../../get_navigation.dart';
3 import '../routes/get_route.dart'; 2 import '../routes/get_route.dart';
4 3
5 -class ParseRouteTree {  
6 - final List<_ParseRouteTreeNode> _nodes = <_ParseRouteTreeNode>[]; 4 +class RouteDecoder {
  5 + final GetPage route;
  6 + final Map<String, String> parameters;
  7 + const RouteDecoder(this.route, this.parameters);
  8 +}
7 9
8 - // bool _hasDefaultRoute = false;  
9 - void addRoute(GetPage route) {  
10 - var path = route.name; 10 +class ParseRouteTree {
  11 + final _routes = <GetPage>[];
11 12
12 - if (path == Navigator.defaultRouteName) {  
13 - // if (_hasDefaultRoute) {  
14 - // throw ("Default route was already defined");  
15 - // }  
16 - var node = _ParseRouteTreeNode(path, _ParseRouteTreeNodeType.component);  
17 - node.routes = [route];  
18 - _nodes.add(node);  
19 - // _hasDefaultRoute = true;  
20 - return; 13 + RouteDecoder matchRoute(String name) {
  14 + final uri = Uri.parse(name);
  15 + final route = _findRoute(uri.path);
  16 + final params = Map<String, String>.from(uri.queryParameters);
  17 + final parsedParams = _parseParams(name, route?.path);
  18 + if (parsedParams != null && parsedParams.isNotEmpty) {
  19 + params.addAll(parsedParams);
21 } 20 }
22 - if (path.startsWith("/")) {  
23 - path = path.substring(1);  
24 - }  
25 - var pathComponents = path.split('/');  
26 - _ParseRouteTreeNode parent;  
27 - for (var i = 0; i < pathComponents.length; i++) {  
28 - var component = pathComponents[i];  
29 - var node = _nodeForComponent(component, parent);  
30 - if (node == null) {  
31 - var type = _typeForComponent(component);  
32 - node = _ParseRouteTreeNode(component, type);  
33 - node.parent = parent;  
34 - if (parent == null) {  
35 - _nodes.add(node);  
36 - } else {  
37 - parent.nodes.add(node);  
38 - }  
39 - }  
40 - if (i == pathComponents.length - 1) {  
41 - if (node.routes == null) {  
42 - node.routes = [route];  
43 - } else {  
44 - node.routes.add(route); 21 + return RouteDecoder(route, params);
45 } 22 }
  23 +
  24 + void addRoutes(List<GetPage> getPages) {
  25 + for (final route in getPages) {
  26 + addRoute(route);
46 } 27 }
47 - parent = node;  
48 } 28 }
49 29
  30 + void addRoute(GetPage route) {
  31 + _routes.add(route);
  32 +
50 // Add Page children. 33 // Add Page children.
51 for (var page in _flattenPage(route)) { 34 for (var page in _flattenPage(route)) {
52 addRoute(page); 35 addRoute(page);
@@ -97,178 +80,37 @@ class ParseRouteTree { @@ -97,178 +80,37 @@ class ParseRouteTree {
97 middlewares: middlewares, 80 middlewares: middlewares,
98 ); 81 );
99 82
100 - _GetPageMatch matchRoute(String path) {  
101 - var usePath = path;  
102 - if (usePath.startsWith("/")) {  
103 - usePath = path.substring(1);  
104 - }  
105 -  
106 - // should take off url parameters first..  
107 - final uri = Uri.tryParse(usePath);  
108 -// List<String> components = usePath.split("/");  
109 - var components = uri.pathSegments;  
110 - if (path == Navigator.defaultRouteName) {  
111 - components = ["/"];  
112 - }  
113 - var nodeMatches = <_ParseRouteTreeNode, _ParseRouteTreeNodeMatch>{};  
114 - var nodesToCheck = _nodes;  
115 - for (final checkComponent in components) {  
116 - final currentMatches = <_ParseRouteTreeNode, _ParseRouteTreeNodeMatch>{};  
117 - final nextNodes = <_ParseRouteTreeNode>[];  
118 - for (final node in nodesToCheck) {  
119 - var pathPart = checkComponent;  
120 - var queryMap = <String, String>{};  
121 -  
122 - if (checkComponent.contains("?") && !checkComponent.contains("=")) {  
123 - var splitParam = checkComponent.split("?");  
124 - pathPart = splitParam[0];  
125 - queryMap = {pathPart: splitParam[1]};  
126 - } else if (checkComponent.contains("?")) {  
127 - var splitParam = checkComponent.split("?");  
128 - var splitParam2 = splitParam[1].split("=");  
129 - if (!splitParam2[1].contains("&")) {  
130 - pathPart = splitParam[0];  
131 - queryMap = {splitParam2[0]: splitParam2[1]};  
132 - } else {  
133 - pathPart = splitParam[0];  
134 - final second = splitParam[1];  
135 - var other = second.split(RegExp(r"[&,=]"));  
136 - for (var i = 0; i < (other.length - 1); i++) {  
137 - var isOdd = (i % 2 == 0);  
138 - if (isOdd) {  
139 - queryMap.addAll({other[0 + i]: other[1 + i]});  
140 - }  
141 - }  
142 - }  
143 - }  
144 -  
145 - final isMatch = (node.part == pathPart || node.isParameter());  
146 - if (isMatch) {  
147 - final parentMatch = nodeMatches[node.parent];  
148 - final match = _ParseRouteTreeNodeMatch.fromMatch(parentMatch, node);  
149 -  
150 - // TODO: find a way to clean this implementation.  
151 - match.parameters.addAll(uri.queryParameters);  
152 -  
153 - if (node.isParameter()) {  
154 - final paramKey = node.part.substring(1);  
155 - match.parameters[paramKey] = pathPart;  
156 - }  
157 - if (queryMap != null) {  
158 - match.parameters.addAll(queryMap);  
159 - }  
160 -  
161 - currentMatches[node] = match;  
162 - if (node.nodes != null) {  
163 - nextNodes.addAll(node.nodes);  
164 - }  
165 - }  
166 - }  
167 - nodeMatches = currentMatches;  
168 - nodesToCheck = nextNodes;  
169 - if (currentMatches.values.length == 0) {  
170 - return null;  
171 - }  
172 - }  
173 - var matches = nodeMatches.values.toList();  
174 - if (matches.length > 0) {  
175 - var match = matches.first;  
176 - var nodeToUse = match.node;  
177 -  
178 - if (nodeToUse != null &&  
179 - nodeToUse.routes != null &&  
180 - nodeToUse.routes.length > 0) {  
181 - var routes = nodeToUse.routes;  
182 - var routeMatch = _GetPageMatch(routes[0]);  
183 -  
184 - routeMatch.parameters = match.parameters;  
185 -  
186 - return routeMatch;  
187 - }  
188 - }  
189 - return null;  
190 - }  
191 -  
192 - _ParseRouteTreeNode _nodeForComponent(  
193 - String component,  
194 - _ParseRouteTreeNode parent,  
195 - ) {  
196 - var nodes = _nodes;  
197 - if (parent != null) {  
198 - nodes = parent.nodes;  
199 - }  
200 - for (var node in nodes) {  
201 - if (node.part == component) {  
202 - return node;  
203 - }  
204 - }  
205 - return null;  
206 - } 83 + GetPage _findRoute(String name) {
  84 + final route = _routes.firstWhere(
  85 + (route) {
  86 + return _match(
  87 + name,
  88 + route.path['regex'] as RegExp,
  89 + );
  90 + },
  91 + orElse: () => null,
  92 + );
207 93
208 - _ParseRouteTreeNodeType _typeForComponent(String component) {  
209 - var type = _ParseRouteTreeNodeType.component;  
210 - if (_isParameterComponent(component)) {  
211 - type = _ParseRouteTreeNodeType.parameter;  
212 - }  
213 - return type; 94 + return route;
214 } 95 }
215 96
216 - bool _isParameterComponent(String component) {  
217 - return component.startsWith(":"); 97 + Map<String, String> _parseParams(
  98 + String path, Map<String, dynamic> routePath) {
  99 + final params = <String, String>{};
  100 + Match paramsMatch = (routePath['regex'] as RegExp).firstMatch(path);
  101 + for (var i = 0; i < (routePath['keys'].length as int); i++) {
  102 + String param;
  103 + try {
  104 + param = Uri.decodeQueryComponent(paramsMatch[i + 1]);
  105 + } on Exception catch (_) {
  106 + param = paramsMatch[i + 1];
218 } 107 }
219 -  
220 - Map<String, String> parseQueryString(String query) {  
221 - var search = RegExp('([^&=]+)=?([^&]*)');  
222 - var params = <String, String>{};  
223 - if (query.startsWith('?')) query = query.substring(1);  
224 - decode(String s) => Uri.decodeComponent(s.replaceAll('+', ' '));  
225 -  
226 - for (Match match in search.allMatches(query)) {  
227 - var key = decode(match.group(1));  
228 - final value = decode(match.group(2));  
229 - params[key] = value; 108 + params[routePath['keys'][i] as String] = param;
230 } 109 }
231 return params; 110 return params;
232 } 111 }
233 -}  
234 -  
235 -class _ParseRouteTreeNodeMatch {  
236 - _ParseRouteTreeNodeMatch(this.node);  
237 -  
238 - _ParseRouteTreeNodeMatch.fromMatch(  
239 - _ParseRouteTreeNodeMatch match, this.node) {  
240 - parameters = <String, String>{};  
241 - if (match != null) {  
242 - parameters.addAll(match.parameters);  
243 - }  
244 - }  
245 -  
246 - _ParseRouteTreeNode node;  
247 - Map<String, String> parameters = <String, String>{};  
248 -}  
249 112
250 -class _ParseRouteTreeNode {  
251 - _ParseRouteTreeNode(this.part, this.type);  
252 -  
253 - String part;  
254 - _ParseRouteTreeNodeType type;  
255 - List<GetPage> routes = <GetPage>[];  
256 - List<_ParseRouteTreeNode> nodes = <_ParseRouteTreeNode>[];  
257 - _ParseRouteTreeNode parent;  
258 -  
259 - bool isParameter() {  
260 - return type == _ParseRouteTreeNodeType.parameter; 113 + bool _match(String name, RegExp path) {
  114 + return path.hasMatch(name);
261 } 115 }
262 } 116 }
263 -  
264 -class _GetPageMatch {  
265 - _GetPageMatch(this.route);  
266 -  
267 - GetPage route;  
268 - Map<String, String> parameters = <String, String>{};  
269 -}  
270 -  
271 -enum _ParseRouteTreeNodeType {  
272 - component,  
273 - parameter,  
274 -}  
@@ -24,8 +24,9 @@ class GetPage { @@ -24,8 +24,9 @@ class GetPage {
24 final RouteSettings settings; 24 final RouteSettings settings;
25 final List<GetPage> children; 25 final List<GetPage> children;
26 final List<GetMiddleware> middlewares; 26 final List<GetMiddleware> middlewares;
27 -  
28 - const GetPage({ 27 + final Map<String, dynamic> path;
  28 + final List<String> keys;
  29 + GetPage({
29 @required this.name, 30 @required this.name,
30 @required this.page, 31 @required this.page,
31 this.title, 32 this.title,
@@ -43,12 +44,40 @@ class GetPage { @@ -43,12 +44,40 @@ class GetPage {
43 this.customTransition, 44 this.customTransition,
44 this.fullscreenDialog = false, 45 this.fullscreenDialog = false,
45 this.children, 46 this.children,
  47 + this.keys,
46 this.middlewares, 48 this.middlewares,
47 - }) : assert(page != null), 49 + }) : path = normalize(name, keysList: keys),
  50 + assert(page != null),
48 assert(name != null), 51 assert(name != null),
49 assert(maintainState != null), 52 assert(maintainState != null),
50 assert(fullscreenDialog != null); 53 assert(fullscreenDialog != null);
51 54
  55 + static Map<String, dynamic> normalize(
  56 + String path, {
  57 + List<String> keysList,
  58 + }) {
  59 + var keys = List<String>.from(keysList ?? const <String>[]);
  60 + var stringPath =
  61 + '$path/?'.replaceAllMapped(RegExp(r'(\.)?:(\w+)(\?)?'), (placeholder) {
  62 + var replace = StringBuffer('(?:');
  63 +
  64 + if (placeholder[1] != null) {
  65 + replace.write('\.');
  66 + }
  67 +
  68 + replace.write('([\\w%+-._~!\$&\'()*,;=:@]+))');
  69 +
  70 + if (placeholder[3] != null) {
  71 + replace.write('?');
  72 + }
  73 +
  74 + keys.add(placeholder[2]);
  75 + return replace.toString();
  76 + }).replaceAll('//', '/');
  77 +
  78 + return {'regex': RegExp('^$stringPath\$'), 'keys': keys};
  79 + }
  80 +
52 GetPage copyWith({ 81 GetPage copyWith({
53 String name, 82 String name,
54 GetPageBuilder page, 83 GetPageBuilder page,
@@ -66,6 +95,7 @@ class GetPage { @@ -66,6 +95,7 @@ class GetPage {
66 Duration transitionDuration, 95 Duration transitionDuration,
67 bool fullscreenDialog, 96 bool fullscreenDialog,
68 RouteSettings settings, 97 RouteSettings settings,
  98 + List<String> keys,
69 List<GetPage> children, 99 List<GetPage> children,
70 List<GetMiddleware> middlewares, 100 List<GetMiddleware> middlewares,
71 }) { 101 }) {
@@ -86,6 +116,7 @@ class GetPage { @@ -86,6 +116,7 @@ class GetPage {
86 transitionDuration: transitionDuration ?? this.transitionDuration, 116 transitionDuration: transitionDuration ?? this.transitionDuration,
87 fullscreenDialog: fullscreenDialog ?? this.fullscreenDialog, 117 fullscreenDialog: fullscreenDialog ?? this.fullscreenDialog,
88 settings: settings ?? this.settings, 118 settings: settings ?? this.settings,
  119 + keys: keys ?? this.keys,
89 children: children ?? this.children, 120 children: children ?? this.children,
90 middlewares: middlewares ?? this.middlewares, 121 middlewares: middlewares ?? this.middlewares,
91 ); 122 );
@@ -102,6 +102,8 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>> @@ -102,6 +102,8 @@ class _GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
102 VoidCallback remove; 102 VoidCallback remove;
103 Object _selector; 103 Object _selector;
104 104
  105 + List<VoidCallback> _removeToOthers = <VoidCallback>[];
  106 +
105 @override 107 @override
106 void initState() { 108 void initState() {
107 super.initState(); 109 super.initState();
@@ -50,4 +50,41 @@ void main() { @@ -50,4 +50,41 @@ void main() {
50 expect(match, isNotNull); 50 expect(match, isNotNull);
51 expect(match.route.name, searchRoute); 51 expect(match.route.name, searchRoute);
52 }); 52 });
  53 +
  54 + testWidgets(
  55 + 'test params from dynamic route',
  56 + (tester) async {
  57 + await tester.pumpWidget(GetMaterialApp(
  58 + initialRoute: '/first/juan',
  59 + getPages: [
  60 + GetPage(page: () => Container(), name: '/first/:name'),
  61 + GetPage(page: () => Container(), name: '/second/:id'),
  62 + GetPage(page: () => Container(), name: '/third'),
  63 + GetPage(page: () => Container(), name: '/last/:id/:name/profile')
  64 + ],
  65 + ));
  66 +
  67 + expect(Get.parameters['name'], 'juan');
  68 +
  69 + Get.toNamed('/second/1234');
  70 +
  71 + await tester.pumpAndSettle();
  72 +
  73 + expect(Get.parameters['id'], '1234');
  74 +
  75 + Get.toNamed('/third?name=jonny&job=dev');
  76 +
  77 + await tester.pumpAndSettle();
  78 +
  79 + expect(Get.parameters['name'], 'jonny');
  80 + expect(Get.parameters['job'], 'dev');
  81 +
  82 + Get.toNamed('/last/1234/ana/profile');
  83 +
  84 + await tester.pumpAndSettle();
  85 +
  86 + expect(Get.parameters['id'], '1234');
  87 + expect(Get.parameters['name'], 'ana');
  88 + },
  89 + );
53 } 90 }