diff --git a/README.md b/README.md index fb370e2..a089fd6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The brightest, hippest, coolest router for Flutter. - Querystring parameter parsing - Common transitions built-in - Simple custom transition creation -- Follows `beta` Flutter channel +- Follows `stable` Flutter channel - Null-safety ## Example Project diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 201d5d2..21ecba2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,15 +23,15 @@ flutter: uses-material-design: true assets: - - assets/images/logo_fluro.png - assets/images/acc_boom.png - - assets/images/ic_result_hz.png - assets/images/ic_function_hz.png + - assets/images/ic_result_hz.png - assets/images/ic_transform_custom_hz.png - assets/images/ic_transform_fade_in_hz.png - assets/images/ic_transform_fade_out_hz.png - assets/images/ic_transform_global_hz.png - assets/images/ic_transform_native_hz.png + - assets/images/logo_fluro.png fonts: - family: Lazer84 diff --git a/lib/src/common.dart b/lib/src/common.dart index 450ce13..ea61ac4 100644 --- a/lib/src/common.dart +++ b/lib/src/common.dart @@ -18,30 +18,44 @@ enum HandlerType { /// The handler to register with [FluroRouter.define] class Handler { - Handler({this.type = HandlerType.route, required this.handlerFunc}); + Handler({ + this.type = HandlerType.route, + required this.handlerFunc, + }); + final HandlerType type; final HandlerFunc handlerFunc; } /// A function that creates new routes. typedef Route RouteCreator( - RouteSettings route, Map> parameters); + RouteSettings route, + Map> parameters, +); /// Builds out a screen based on string path [parameters] and context. /// /// Note: you can access [RouteSettings] with the [context.settings] extension typedef Widget? HandlerFunc( - BuildContext? context, Map> parameters); + BuildContext? context, + Map> parameters, +); /// A route that is added to the router tree. class AppRoute { + AppRoute( + this.route, + this.handler, { + this.transitionType, + this.transitionDuration, + this.transitionBuilder, + }); + String route; dynamic handler; TransitionType? transitionType; Duration? transitionDuration; RouteTransitionsBuilder? transitionBuilder; - AppRoute(this.route, this.handler, - {this.transitionType, this.transitionDuration, this.transitionBuilder}); } /// The type of transition to use when pushing/popping a route. @@ -72,10 +86,12 @@ enum RouteMatchType { /// The route that was matched. class RouteMatch { - RouteMatch( - {this.matchType = RouteMatchType.noMatch, - this.route, - this.errorMessage = "Unable to match route. Please check the logs."}); + RouteMatch({ + this.matchType = RouteMatchType.noMatch, + this.route, + this.errorMessage = "Unable to match route. Please check the logs.", + }); + final Route? route; final RouteMatchType matchType; final String errorMessage; @@ -83,9 +99,13 @@ class RouteMatch { /// When the route is not found. class RouteNotFoundException implements Exception { + RouteNotFoundException( + this.message, + this.path, + ); + final String message; final String path; - RouteNotFoundException(this.message, this.path); @override String toString() { diff --git a/lib/src/fluro_router.dart b/lib/src/fluro_router.dart index 733ece2..31a7d34 100644 --- a/lib/src/fluro_router.dart +++ b/lib/src/fluro_router.dart @@ -29,20 +29,22 @@ class FluroRouter { static final appRouter = FluroRouter(); /// The tree structure that stores the defined routes - final RouteTree _routeTree = RouteTree(); + final _routeTree = RouteTree(); /// Generic handler for when a route has not been defined Handler? notFoundHandler; /// The default transition duration to use throughout Fluro - static const defaultTransitionDuration = const Duration(milliseconds: 250); + static const defaultTransitionDuration = Duration(milliseconds: 250); /// Creates a [PageRoute] definition for the passed [RouteHandler]. You can optionally provide a default transition type. - void define(String routePath, - {required Handler? handler, - TransitionType? transitionType, - Duration transitionDuration = defaultTransitionDuration, - RouteTransitionsBuilder? transitionBuilder}) { + void define( + String routePath, { + required Handler? handler, + TransitionType? transitionType, + Duration transitionDuration = defaultTransitionDuration, + RouteTransitionsBuilder? transitionBuilder, + }) { _routeTree.addRoute( AppRoute(routePath, handler, transitionType: transitionType, @@ -62,30 +64,39 @@ class FluroRouter { Navigator.of(context).pop(result); /// 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, - TransitionType? transition, - Duration? transitionDuration, - RouteTransitionsBuilder? transitionBuilder, - RouteSettings? routeSettings}) { - RouteMatch routeMatch = matchRoute(context, path, - transitionType: transition, - transitionsBuilder: transitionBuilder, - transitionDuration: transitionDuration, - maintainState: maintainState, - routeSettings: routeSettings); + Future navigateTo( + BuildContext context, + String path, { + bool replace = false, + bool clearStack = false, + bool maintainState = true, + bool rootNavigator = false, + TransitionType? transition, + Duration? transitionDuration, + RouteTransitionsBuilder? transitionBuilder, + RouteSettings? routeSettings, + }) { + RouteMatch routeMatch = matchRoute( + context, + path, + transitionType: transition, + transitionsBuilder: transitionBuilder, + transitionDuration: transitionDuration, + maintainState: maintainState, + routeSettings: routeSettings, + ); + Route? route = routeMatch.route; Completer completer = Completer(); Future future = completer.future; + if (routeMatch.matchType == RouteMatchType.nonVisual) { 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); if (clearStack) { @@ -106,33 +117,44 @@ class FluroRouter { return future; } - Route _notFoundRoute(BuildContext context, String path, - {bool? maintainState}) { - RouteCreator creator = - (RouteSettings? routeSettings, Map> parameters) { + Route _notFoundRoute( + BuildContext context, + String path, { + bool? maintainState, + }) { + RouteCreator creator = ( + RouteSettings? routeSettings, + Map> parameters, + ) { return MaterialPageRoute( - settings: routeSettings, - maintainState: maintainState ?? true, - builder: (BuildContext context) { - return notFoundHandler?.handlerFunc(context, parameters) ?? - SizedBox.shrink(); - }); + settings: routeSettings, + maintainState: maintainState ?? true, + builder: (BuildContext context) { + return notFoundHandler?.handlerFunc(context, parameters) ?? + SizedBox.shrink(); + }, + ); }; + return creator(RouteSettings(name: path), {}); } /// 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}) { + RouteMatch matchRoute( + BuildContext? buildContext, + String? path, { + RouteSettings? routeSettings, + TransitionType? transitionType, + Duration? transitionDuration, + RouteTransitionsBuilder? transitionsBuilder, + bool maintainState = true, + }) { RouteSettings settingsToUse = routeSettings ?? RouteSettings(name: path); if (settingsToUse.name == null) { settingsToUse = settingsToUse.copyWith(name: path); } + AppRouteMatch? match = _routeTree.matchRoute(path!); AppRoute? route = match?.route; @@ -142,56 +164,66 @@ class FluroRouter { Handler handler = (route != null ? route.handler : notFoundHandler); TransitionType? transition = transitionType; + if (transitionType == null) { transition = route != null ? route.transitionType : TransitionType.native; } + if (route == null && notFoundHandler == null) { return RouteMatch( - matchType: RouteMatchType.noMatch, - errorMessage: "No matching route was found"); + matchType: RouteMatchType.noMatch, + errorMessage: "No matching route was found", + ); } - Map> parameters = - match?.parameters ?? >{}; + + final parameters = match?.parameters ?? >{}; + if (handler.type == HandlerType.function) { handler.handlerFunc(buildContext, parameters); return RouteMatch(matchType: RouteMatchType.nonVisual); } - RouteCreator creator = - (RouteSettings? routeSettings, Map> parameters) { + RouteCreator creator = ( + RouteSettings? routeSettings, + Map> parameters, + ) { bool isNativeTransition = (transition == TransitionType.native || transition == TransitionType.nativeModal); + if (isNativeTransition) { return MaterialPageRoute( - settings: routeSettings, - fullscreenDialog: transition == TransitionType.nativeModal, - maintainState: maintainState, - builder: (BuildContext context) { - return handler.handlerFunc(context, parameters) ?? - SizedBox.shrink(); - }); + settings: routeSettings, + fullscreenDialog: transition == TransitionType.nativeModal, + maintainState: maintainState, + builder: (BuildContext context) { + return handler.handlerFunc(context, parameters) ?? + SizedBox.shrink(); + }, + ); } else if (transition == TransitionType.material || transition == TransitionType.materialFullScreenDialog) { return MaterialPageRoute( - settings: routeSettings, - fullscreenDialog: - transition == TransitionType.materialFullScreenDialog, - maintainState: maintainState, - builder: (BuildContext context) { - return handler.handlerFunc(context, parameters) ?? - SizedBox.shrink(); - }); + settings: routeSettings, + fullscreenDialog: + transition == TransitionType.materialFullScreenDialog, + maintainState: maintainState, + builder: (BuildContext context) { + return handler.handlerFunc(context, parameters) ?? + SizedBox.shrink(); + }, + ); } else if (transition == TransitionType.cupertino || transition == TransitionType.cupertinoFullScreenDialog) { return CupertinoPageRoute( - settings: routeSettings, - fullscreenDialog: - transition == TransitionType.cupertinoFullScreenDialog, - maintainState: maintainState, - builder: (BuildContext context) { - return handler.handlerFunc(context, parameters) ?? - SizedBox.shrink(); - }); + settings: routeSettings, + fullscreenDialog: + transition == TransitionType.cupertinoFullScreenDialog, + maintainState: maintainState, + builder: (BuildContext context) { + return handler.handlerFunc(context, parameters) ?? + SizedBox.shrink(); + }, + ); } else { RouteTransitionsBuilder? routeTransitionsBuilder; @@ -226,6 +258,7 @@ class FluroRouter { ); } }; + return RouteMatch( matchType: RouteMatchType.visual, route: creator(settingsToUse, parameters), @@ -234,17 +267,22 @@ class FluroRouter { RouteTransitionsBuilder _standardTransitionsBuilder( TransitionType? transitionType) { - return (BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { + return ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { if (transitionType == TransitionType.fadeIn) { return FadeTransition(opacity: animation, child: child); } else { - const Offset topLeft = const Offset(0.0, 0.0); - const Offset topRight = const Offset(1.0, 0.0); - const Offset bottomLeft = const Offset(0.0, 1.0); + const topLeft = const Offset(0.0, 0.0); + const topRight = const Offset(1.0, 0.0); + const bottomLeft = const Offset(0.0, 1.0); + + var startOffset = bottomLeft; + var endOffset = topLeft; - Offset startOffset = bottomLeft; - Offset endOffset = topLeft; if (transitionType == TransitionType.inFromLeft) { startOffset = const Offset(-1.0, 0.0); endOffset = topLeft; @@ -274,8 +312,12 @@ class FluroRouter { /// if any defined handler is found. It can also be used with the [MaterialApp.onGenerateRoute] /// property as callback to create routes that can be used with the [Navigator] class. Route? generator(RouteSettings routeSettings) { - RouteMatch match = - matchRoute(null, routeSettings.name, routeSettings: routeSettings); + RouteMatch match = matchRoute( + null, + routeSettings.name, + routeSettings: routeSettings, + ); + return match.route; } diff --git a/lib/src/tree.dart b/lib/src/tree.dart index 965ee48..9f21567 100644 --- a/lib/src/tree.dart +++ b/lib/src/tree.dart @@ -18,43 +18,40 @@ enum RouteTreeNodeType { /// A matched [AppRoute] class AppRouteMatch { - // constructors AppRouteMatch(this.route); - // properties AppRoute route; Map> parameters = >{}; } /// A matched [RouteTreeNode] class RouteTreeNodeMatch { - // constructors RouteTreeNodeMatch(this.node); + RouteTreeNode node; + + var parameters = >{}; + RouteTreeNodeMatch.fromMatch(RouteTreeNodeMatch? match, this.node) { parameters = >{}; if (match != null) { parameters.addAll(match.parameters); } } - - // properties - RouteTreeNode node; - Map> parameters = >{}; } /// A node on [RouteTree] class RouteTreeNode { - // constructors RouteTreeNode(this.part, this.type); - // properties String part; RouteTreeNodeType? type; - List routes = []; - List nodes = []; + RouteTreeNode? parent; + var routes = []; + var nodes = []; + bool isParameter() { return type == RouteTreeNodeType.parameter; } @@ -62,11 +59,10 @@ class RouteTreeNode { /// A [RouteTree] class RouteTree { - // private final List _nodes = []; bool _hasDefaultRoute = false; - // addRoute - add a route to the route tree + /// Add a route to the route tree void addRoute(AppRoute route) { String path = route.route; // is root/default route, just add it @@ -76,69 +72,83 @@ class RouteTree { // could be affected throw ("Default route was already defined"); } + var node = RouteTreeNode(path, RouteTreeNodeType.component); node.routes = [route]; _nodes.add(node); _hasDefaultRoute = true; return; } + if (path.startsWith("/")) { path = path.substring(1); } - List pathComponents = path.split('/'); + + final 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) { _nodes.add(node); } else { parent.nodes.add(node); } } + if (i == pathComponents.length - 1) { node.routes.add(route); } + parent = node; } } AppRouteMatch? matchRoute(String path) { - String usePath = path; + var usePath = path; + if (usePath.startsWith("/")) { usePath = path.substring(1); } - List components = usePath.split("/"); + + var components = usePath.split("/"); + if (path == Navigator.defaultRouteName) { components = ["/"]; } - Map nodeMatches = - {}; - List nodesToCheck = _nodes; - for (String checkComponent in components) { - Map currentMatches = - {}; - List nextNodes = []; - String pathPart = checkComponent; + var nodeMatches = {}; + var nodesToCheck = _nodes; + + for (final checkComponent in components) { + final currentMatches = {}; + final nextNodes = []; + + var pathPart = checkComponent; Map>? 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()); + + for (final node in nodesToCheck) { + final isMatch = (node.part == pathPart || node.isParameter()); + if (isMatch) { RouteTreeNodeMatch? parentMatch = nodeMatches[node.parent]; - RouteTreeNodeMatch match = - RouteTreeNodeMatch.fromMatch(parentMatch, node); + final match = RouteTreeNodeMatch.fromMatch(parentMatch, node); if (node.isParameter()) { - String paramKey = node.part.substring(1); + final paramKey = node.part.substring(1); match.parameters[paramKey] = [pathPart]; } if (queryMap != null) { @@ -148,23 +158,29 @@ class RouteTree { nextNodes.addAll(node.nodes); } } + nodeMatches = currentMatches; nodesToCheck = nextNodes; + if (currentMatches.values.length == 0) { return null; } } - List matches = nodeMatches.values.toList(); + + final matches = nodeMatches.values.toList(); + if (matches.isNotEmpty) { - RouteTreeNodeMatch match = matches.first; - RouteTreeNode? nodeToUse = match.node; + final match = matches.first; + final nodeToUse = match.node; final routes = nodeToUse.routes; + if (routes.isNotEmpty) { - AppRouteMatch routeMatch = AppRouteMatch(routes[0]); + final routeMatch = AppRouteMatch(routes[0]); routeMatch.parameters = match.parameters; return routeMatch; } } + return null; } @@ -173,13 +189,17 @@ class RouteTree { } void _printSubTree({RouteTreeNode? parent, int level = 0}) { - List nodes = parent != null ? parent.nodes : _nodes; - for (RouteTreeNode node in nodes) { - String indent = ""; - for (int i = 0; i < level; i++) { + final nodes = parent != null ? parent.nodes : _nodes; + + for (final node in nodes) { + var indent = ""; + + for (var i = 0; i < level; i++) { indent += " "; } + print("$indent${node.part}: total routes=${node.routes.length}"); + if (node.nodes.isNotEmpty) { _printSubTree(parent: node, level: level + 1); } @@ -187,24 +207,29 @@ class RouteTree { } RouteTreeNode? _nodeForComponent(String component, RouteTreeNode? parent) { - List nodes = _nodes; + var nodes = _nodes; + if (parent != null) { // search parent for sub-node matches nodes = parent.nodes; } - for (RouteTreeNode node in nodes) { + + for (final node in nodes) { if (node.part == component) { return node; } } + return null; } RouteTreeNodeType _typeForComponent(String component) { - RouteTreeNodeType type = RouteTreeNodeType.component; + var type = RouteTreeNodeType.component; + if (_isParameterComponent(component)) { type = RouteTreeNodeType.parameter; } + return type; } @@ -216,11 +241,14 @@ class RouteTree { Map> parseQueryString(String query) { final search = RegExp('([^&=]+)=?([^&]*)'); final params = Map>(); + if (query.startsWith('?')) query = query.substring(1); + decode(String s) => Uri.decodeComponent(s.replaceAll('+', ' ')); + for (Match match in search.allMatches(query)) { - String key = decode(match.group(1)!); - String value = decode(match.group(2)!); + final key = decode(match.group(1)!); + final value = decode(match.group(2)!); if (params.containsKey(key)) { params[key]!.add(value); @@ -228,6 +256,7 @@ class RouteTree { params[key] = [value]; } } + return params; } }