You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
homepage/content/post/20201008-flutter-simple-rou...

351 lines
8.9 KiB
Markdown

---
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.
<!--more-->
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<dynamic> 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<String, String> extractNamedArgs(BuildContext context);
String buildUri({Map<String, String> 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<String, String> extractNamedArgs(BuildContext context) => {};
String buildUri({Map<String, String> buildArgs}) {
assert(buildArgs == null);
return uri;
}
}
class NamedArgsRoute implements GlobalRoute {
final String key;
final String builderUri;
final RegExp regex;
final Map<String, int> 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<String, String> extractNamedArgs(BuildContext context) {
var uri = ModalRoute.of(context).settings.name;
if (!matchesRoute(uri)) {
return {};
}
final match = regex.firstMatch(uri);
Map<String, String> 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<String, String> 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<String, int> 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<String, GlobalRoute> routes = {};
List<GlobalRoute> dynamicRoutes = [];
Map<String, ExactRoute> exactRoutes = {};
final List<String> 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 extends GlobalRoute>(T route) {
routes[route.key] = route;
if (route is ExactRoute) {
exactRoutes[route.uri] = route;
} else {
dynamicRoutes.add(route);
}
}
String buildUri(String key, {Map<String, String> buildArgs}) {
return routes[key].buildUri(buildArgs: buildArgs);
}
Map<String, String> 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<dynamic> 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<void> 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.