--- date: 2020-09-08T23:00:00+01:00 title: Flutter simple router author: jochum tags: - flutter - Let's Check --- For the [Let's Check](https://forum.checkmk.com/t/lets-check-an-android-ios-app-for-check-mk/20895/2) App I'm writing I needed a simple router. I haven't found anything that suited my needs so I decided to role my own. My implementation supports: * Regex named Args * All routes named, this allows usage like: ( `GlobalRouter().buildUri(routeSettingsConnection, buildArgs: {"alias": "JOCHUM"});`) * Static/Dynamic routes (Static string or Regex) * Dynamicaly register/deregister routes as needed #### The Router implementation I have this saved as **GlobalRouter.dart** ```dart import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; const routeHome = 'home'; const routeSplash = 'splash'; const routeSettings = 'settings'; const routeSettingsConnection = 'settings_connection'; const routeNotFound = 'not_found'; const routeHosts = 'hosts'; const routeServices = 'services'; const routeHost = 'host'; const routeService = 'service'; typedef RouteBuilder = Route Function(RouteSettings context); class BuildError implements Exception { final String message; BuildError(this.message); String toString() => message; } abstract class GlobalRoute { String get key; RouteBuilder get route; bool matchesRoute(String route); Map extractNamedArgs(BuildContext context); String buildUri({Map buildArgs}); } class ExactRoute implements GlobalRoute { final String key; final String uri; final RouteBuilder route; ExactRoute({@required this.key, @required this.uri, @required this.route}); bool matchesRoute(String route) => route == uri; Map extractNamedArgs(BuildContext context) => {}; String buildUri({Map buildArgs}) { assert(buildArgs == null); return uri; } } class NamedArgsRoute implements GlobalRoute { final String key; final String builderUri; final RegExp regex; final Map args; final RouteBuilder route; final bool lastArgOptional; NamedArgsRoute( {@required this.key, @required this.builderUri, @required this.regex, @required this.args, @required this.route, this.lastArgOptional = false}); bool matchesRoute(String route) { return regex.hasMatch(route); } Map extractNamedArgs(BuildContext context) { var uri = ModalRoute.of(context).settings.name; if (!matchesRoute(uri)) { return {}; } final match = regex.firstMatch(uri); Map result = {}; for (var name in args.keys) { if (match.groupCount >= args[name] && match.group(args[name]) != null) { result[name] = Uri.decodeComponent(match.group(args[name])); } } return result; } String buildUri({Map buildArgs}) { if (buildArgs == null && lastArgOptional && args.length == 1) { return builderUri.replaceFirst(r'/{' + args.keys.first + r'}', ""); } else if (buildArgs == null) { throw new BuildError("BuildArgs are not optional for route '$key'"); } if (lastArgOptional && buildArgs.keys.length < args.keys.length - 1) { throw new BuildError("Not all args given for route '$key'"); } else if (buildArgs.keys.length < args.keys.length) { throw new BuildError("Not all args given for route '$key'"); } var result = builderUri; for (var argName in buildArgs.keys) { result = result.replaceAll('{$argName}', Uri.encodeComponent(buildArgs[argName])); } if (lastArgOptional && result.contains('{')) { result = result.replaceFirst(RegExp(r"(\/?\{\S+\})$"), ""); } return result; } } GlobalRoute buildRoute( {@required String key, @required String uri, bool lastArgOptional = false, RouteBuilder route}) { var matches = RegExp(r"\{(\w+)\}").allMatches(uri); if (!matches.isNotEmpty) { return ExactRoute(key: key, uri: uri, route: route); } Map args = {}; var regex = r'^' + uri.replaceAll("/", r"\/") + r'$'; var i = 1; for (var match in matches) { if (lastArgOptional && i == matches.length) { regex = regex.replaceFirst(r'\/{' + match.group(1) + r'}', r"((\/([^\/]+))?)\/?"); args[match.group(1)] = i + 2; break; } regex = regex.replaceFirst('{' + match.group(1) + '}', r"([^\/]+)"); args[match.group(1)] = i; i++; } return NamedArgsRoute( key: key, builderUri: uri, regex: new RegExp(regex), args: args, route: route, lastArgOptional: lastArgOptional); } class GlobalRouter { Map routes = {}; List dynamicRoutes = []; Map exactRoutes = {}; final List requiredRoutes = [ routeHome, routeSplash, routeSettings, routeSettingsConnection, routeNotFound ]; static final GlobalRouter _singleton = GlobalRouter._internal(); GlobalRouter._internal(); factory GlobalRouter() { return _singleton; } bool validateRoutes() { requiredRoutes.forEach((name) { if (!routes.containsKey(name)) { return false; } }); return true; } void clear() { routes.clear(); exactRoutes.clear(); dynamicRoutes.clear(); } void add(T route) { routes[route.key] = route; if (route is ExactRoute) { exactRoutes[route.uri] = route; } else { dynamicRoutes.add(route); } } String buildUri(String key, {Map buildArgs}) { return routes[key].buildUri(buildArgs: buildArgs); } Map extractNamedArgs(BuildContext context, String key) { return routes[key].extractNamedArgs(context); } bool isCurrentRoute(BuildContext context, String key) { return routes[key].matchesRoute(ModalRoute.of(context).settings.name); } Route generateRoute(RouteSettings context) { if (kDebugMode) { print("Generating route for '${context.name}'"); } if (exactRoutes.containsKey(context.name)) { if (kDebugMode) { print("... found route: ${context.name}"); } return exactRoutes[context.name].route(context); } for (var route in dynamicRoutes) { if (route.matchesRoute(context.name)) { if (kDebugMode) { print("... found route: ${route.key}"); } return route.route(context); } } print("... going to 404"); return routes[routeNotFound].route(context); } } ``` #### Usage of GlobalRouter This is how a static route definition looks like: ```dart class HomeScreen extends BaseSlimScreen { static final route = buildRoute( key: routeHome, uri: "/", route: (context) => MaterialPageRoute( settings: context, builder: (context) => HomeScreen(), )); ``` And this is a Regex Route: ```dart class HostScreen extends BaseSlimScreen { static final route = buildRoute( key: routeHost, uri: "/conn/{alias}/host/{hostname}", lastArgOptional: false, route: (context) => MaterialPageRoute( settings: context, builder: (context) => HostScreen(), )); ``` Somewhere I have register Routes with GlobalRouter(): File is **lib/screen/slim/slim_router.dart** ```dart import '../../global_router.dart'; import 'splash_screen.dart'; import 'home_screen.dart'; import 'settings_screen.dart'; import 'settings_connection_screen.dart'; import 'not_found_screen.dart'; import 'hosts_screen.dart'; import 'services_screen.dart'; import 'host_screen.dart'; import 'service_screen.dart'; export '../../global_router.dart'; void registerSlimRoutes() { GlobalRouter().add(HomeScreen.route); GlobalRouter().add(SplashScreen.route); GlobalRouter().add(SettingsScreen.route); GlobalRouter().add(SettingsConnectionScreen.route); GlobalRouter().add(NotFoundScreen.route); GlobalRouter().add(HostsScreen.route); GlobalRouter().add(ServicesScreen.route); GlobalRouter().add(HostScreen.route); GlobalRouter().add(ServiceScreen.route); assert(GlobalRouter().validateRoutes()); } ``` In **main.dart** i configure the router: ```dart Future main() async { var mediaWidth = MediaQueryData.fromWindow(window).size.width; mediaWidth >= ultraWideLayoutThreshold ? registerSlimRoutes() // UltraWide : mediaWidth > wideLayoutThreshold ? registerSlimRoutes() // Wide : registerSlimRoutes(); // Slim } ``` And with that GlobalRouter() is in use: ```dart class App extends StatelessWidget { App({Key key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( ... onGenerateRoute: (routeContext) => GlobalRouter().generateRoute(routeContext), ); } } ``` #### License This is MIT Licensed do whatever you want with it but don't blame me. I hope it helps you to make your own Router.