clean route parser implementation and add test to dynamic routes
Showing
7 changed files
with
123 additions
and
210 deletions
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. |
lib/get_connect.dart
0 → 100644
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 | } |
-
Please register or login to post a comment