fluro_router.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. * fluro
  3. * Created by Yakka
  4. * https://theyakka.com
  5. *
  6. * Copyright (c) 2019 Yakka, LLC. All rights reserved.
  7. * See LICENSE for distribution and usage details.
  8. */
  9. import 'dart:async';
  10. import 'package:fluro/fluro.dart';
  11. import 'package:fluro/src/common.dart';
  12. import 'package:flutter/foundation.dart';
  13. import 'package:flutter/cupertino.dart';
  14. import 'package:flutter/material.dart';
  15. /// {@template fluro_router}
  16. /// Attach [FluroRouter] to [MaterialApp] by connnecting [FluroRouter.generator] to [MaterialApp.onGenerateRoute].
  17. ///
  18. /// Define routes with [FluroRouter.define], optionally specifying transition types and connecting string path params to
  19. /// your screen widget's constructor.
  20. ///
  21. /// Push new route paths with [FluroRouter.appRouter.navigateTo] or continue to use [Navigator.of(context).push] if you prefer.
  22. /// {@endtemplate}
  23. class FluroRouter {
  24. /// The static / singleton instance of [FluroRouter]
  25. ///
  26. /// {@macro fluro_router}
  27. static final appRouter = FluroRouter();
  28. /// The tree structure that stores the defined routes
  29. final RouteTree _routeTree = RouteTree();
  30. /// Generic handler for when a route has not been defined
  31. Handler notFoundHandler;
  32. /// Creates a [PageRoute] definition for the passed [RouteHandler]. You can optionally provide a default transition type.
  33. void define(String routePath,
  34. {@required Handler handler,
  35. TransitionType transitionType,
  36. Duration transitionDuration = const Duration(milliseconds: 250),
  37. RouteTransitionsBuilder transitionBuilder}) {
  38. _routeTree.addRoute(
  39. AppRoute(routePath, handler,
  40. transitionType: transitionType,
  41. transitionDuration: transitionDuration,
  42. transitionBuilder: transitionBuilder),
  43. );
  44. }
  45. /// Finds a defined [AppRoute] for the path value. If no [AppRoute] definition was found
  46. /// then function will return null.
  47. AppRouteMatch match(String path) {
  48. return _routeTree.matchRoute(path);
  49. }
  50. /// Similar to [Navigator.pop]
  51. void pop<T>(BuildContext context, [T result]) =>
  52. Navigator.of(context).pop(result);
  53. /// Similar to [Navigator.push] but with a few extra features.
  54. Future navigateTo(BuildContext context, String path,
  55. {bool replace = false,
  56. bool clearStack = false,
  57. bool maintainState = true,
  58. bool rootNavigator = false,
  59. bool nullOk = false,
  60. TransitionType transition,
  61. Duration transitionDuration,
  62. RouteTransitionsBuilder transitionBuilder,
  63. RouteSettings routeSettings}) {
  64. RouteMatch routeMatch = matchRoute(context, path,
  65. transitionType: transition,
  66. transitionsBuilder: transitionBuilder,
  67. transitionDuration: transitionDuration,
  68. maintainState: maintainState,
  69. routeSettings: routeSettings);
  70. Route<dynamic> route = routeMatch.route;
  71. Completer completer = Completer();
  72. Future future = completer.future;
  73. if (routeMatch.matchType == RouteMatchType.nonVisual) {
  74. completer.complete("Non visual route type.");
  75. } else {
  76. if (route == null && notFoundHandler != null) {
  77. route = _notFoundRoute(context, path, maintainState: maintainState);
  78. }
  79. if (route != null) {
  80. final navigator =
  81. Navigator.of(context, rootNavigator: rootNavigator, nullOk: nullOk);
  82. if (clearStack) {
  83. future = navigator.pushAndRemoveUntil(route, (check) => false);
  84. } else {
  85. future = replace
  86. ? navigator.pushReplacement(route)
  87. : navigator.push(route);
  88. }
  89. completer.complete();
  90. } else {
  91. String error = "No registered route was found to handle '$path'.";
  92. print(error);
  93. completer.completeError(RouteNotFoundException(error, path));
  94. }
  95. }
  96. return future;
  97. }
  98. Route<Null> _notFoundRoute(BuildContext context, String path,
  99. {bool maintainState}) {
  100. RouteCreator<Null> creator =
  101. (RouteSettings routeSettings, Map<String, List<String>> parameters) {
  102. return MaterialPageRoute<Null>(
  103. settings: routeSettings,
  104. maintainState: maintainState,
  105. builder: (BuildContext context) {
  106. return notFoundHandler.handlerFunc(context, parameters);
  107. });
  108. };
  109. return creator(RouteSettings(name: path), null);
  110. }
  111. /// Attempt to match a route to the provided [path].
  112. RouteMatch matchRoute(BuildContext buildContext, String path,
  113. {RouteSettings routeSettings,
  114. TransitionType transitionType,
  115. Duration transitionDuration,
  116. RouteTransitionsBuilder transitionsBuilder,
  117. bool maintainState = true}) {
  118. RouteSettings settingsToUse = routeSettings;
  119. if (routeSettings == null) {
  120. settingsToUse = RouteSettings(name: path);
  121. }
  122. if (settingsToUse.name == null) {
  123. settingsToUse = settingsToUse.copyWith(name: path);
  124. }
  125. AppRouteMatch match = _routeTree.matchRoute(path);
  126. AppRoute route = match?.route;
  127. if (route.transitionDuration != null) {
  128. transitionDuration = route.transitionDuration;
  129. }
  130. Handler handler = (route != null ? route.handler : notFoundHandler);
  131. var transition = transitionType;
  132. if (transitionType == null) {
  133. transition = route != null ? route.transitionType : TransitionType.native;
  134. }
  135. if (route == null && notFoundHandler == null) {
  136. return RouteMatch(
  137. matchType: RouteMatchType.noMatch,
  138. errorMessage: "No matching route was found");
  139. }
  140. Map<String, List<String>> parameters =
  141. match?.parameters ?? <String, List<String>>{};
  142. if (handler.type == HandlerType.function) {
  143. handler.handlerFunc(buildContext, parameters);
  144. return RouteMatch(matchType: RouteMatchType.nonVisual);
  145. }
  146. RouteCreator creator =
  147. (RouteSettings routeSettings, Map<String, List<String>> parameters) {
  148. bool isNativeTransition = (transition == TransitionType.native ||
  149. transition == TransitionType.nativeModal);
  150. if (isNativeTransition) {
  151. if (Theme.of(buildContext).platform == TargetPlatform.iOS) {
  152. return CupertinoPageRoute<dynamic>(
  153. settings: routeSettings,
  154. fullscreenDialog: transition == TransitionType.nativeModal,
  155. maintainState: maintainState,
  156. builder: (BuildContext context) {
  157. return handler.handlerFunc(context, parameters);
  158. });
  159. } else {
  160. return MaterialPageRoute<dynamic>(
  161. settings: routeSettings,
  162. fullscreenDialog: transition == TransitionType.nativeModal,
  163. maintainState: maintainState,
  164. builder: (BuildContext context) {
  165. return handler.handlerFunc(context, parameters);
  166. });
  167. }
  168. } else if (transition == TransitionType.material ||
  169. transition == TransitionType.materialFullScreenDialog) {
  170. return MaterialPageRoute<dynamic>(
  171. settings: routeSettings,
  172. fullscreenDialog:
  173. transition == TransitionType.materialFullScreenDialog,
  174. maintainState: maintainState,
  175. builder: (BuildContext context) {
  176. return handler.handlerFunc(context, parameters);
  177. });
  178. } else if (transition == TransitionType.cupertino ||
  179. transition == TransitionType.cupertinoFullScreenDialog) {
  180. return CupertinoPageRoute<dynamic>(
  181. settings: routeSettings,
  182. fullscreenDialog:
  183. transition == TransitionType.cupertinoFullScreenDialog,
  184. maintainState: maintainState,
  185. builder: (BuildContext context) {
  186. return handler.handlerFunc(context, parameters);
  187. });
  188. } else {
  189. var routeTransitionsBuilder;
  190. if (transition == TransitionType.custom) {
  191. routeTransitionsBuilder =
  192. transitionsBuilder ?? route.transitionBuilder;
  193. } else {
  194. routeTransitionsBuilder = _standardTransitionsBuilder(transition);
  195. }
  196. return PageRouteBuilder<dynamic>(
  197. settings: routeSettings,
  198. maintainState: maintainState,
  199. pageBuilder: (BuildContext context, Animation<double> animation,
  200. Animation<double> secondaryAnimation) {
  201. return handler.handlerFunc(context, parameters);
  202. },
  203. transitionDuration: transition == TransitionType.none
  204. ? Duration.zero
  205. : transitionDuration ?? route.transitionDuration,
  206. reverseTransitionDuration: transition == TransitionType.none
  207. ? Duration.zero
  208. : transitionDuration ?? route.transitionDuration,
  209. transitionsBuilder: transition == TransitionType.none
  210. ? (_, __, ___, child) => child
  211. : routeTransitionsBuilder,
  212. );
  213. }
  214. };
  215. return RouteMatch(
  216. matchType: RouteMatchType.visual,
  217. route: creator(settingsToUse, parameters),
  218. );
  219. }
  220. RouteTransitionsBuilder _standardTransitionsBuilder(
  221. TransitionType transitionType) {
  222. return (BuildContext context, Animation<double> animation,
  223. Animation<double> secondaryAnimation, Widget child) {
  224. if (transitionType == TransitionType.fadeIn) {
  225. return FadeTransition(opacity: animation, child: child);
  226. } else {
  227. const Offset topLeft = const Offset(0.0, 0.0);
  228. const Offset topRight = const Offset(1.0, 0.0);
  229. const Offset bottomLeft = const Offset(0.0, 1.0);
  230. Offset startOffset = bottomLeft;
  231. Offset endOffset = topLeft;
  232. if (transitionType == TransitionType.inFromLeft) {
  233. startOffset = const Offset(-1.0, 0.0);
  234. endOffset = topLeft;
  235. } else if (transitionType == TransitionType.inFromRight) {
  236. startOffset = topRight;
  237. endOffset = topLeft;
  238. } else if (transitionType == TransitionType.inFromBottom) {
  239. startOffset = bottomLeft;
  240. endOffset = topLeft;
  241. } else if (transitionType == TransitionType.inFromTop) {
  242. startOffset = Offset(0.0, -1.0);
  243. endOffset = topLeft;
  244. }
  245. return SlideTransition(
  246. position: Tween<Offset>(
  247. begin: startOffset,
  248. end: endOffset,
  249. ).animate(animation),
  250. child: child,
  251. );
  252. }
  253. };
  254. }
  255. /// Route generation method. This function can be used as a way to create routes on-the-fly
  256. /// if any defined handler is found. It can also be used with the [MaterialApp.onGenerateRoute]
  257. /// property as callback to create routes that can be used with the [Navigator] class.
  258. Route<dynamic> generator(RouteSettings routeSettings) {
  259. RouteMatch match =
  260. matchRoute(null, routeSettings.name, routeSettings: routeSettings);
  261. return match.route;
  262. }
  263. /// Prints the route tree so you can analyze it.
  264. void printTree() {
  265. _routeTree.printTree();
  266. }
  267. }