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.
351 lines
8.9 KiB
Markdown
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. |