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