Skip to content

Instantly share code, notes, and snippets.

@jezell
Last active October 4, 2024 08:56
Show Gist options
  • Save jezell/0aa4c66990d17d342efedb19b7621381 to your computer and use it in GitHub Desktop.
Save jezell/0aa4c66990d17d342efedb19b7621381 to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:uri/uri.dart';
class PathRouteMatch {
PathRouteMatch(
{required this.parameters,
required this.route,
required this.builder,
required this.uri,
this.extra});
/// The [Uri] that matched this route
final Uri uri;
/// Parameters pulled from the [UriTemplate] associated with this route
final Map<String, String?> parameters;
/// The [PathRoute] that matched this route
final PathRoute route;
/// Extra data passed to the route
final Object? extra;
/// Build the [Widget] to display for this route
Widget Function(BuildContext context, PathRouteMatch route) builder;
/// Lookup the [PathRouteMatch] from the current context
static PathRouteMatch of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_UriRouteData>()!.route;
}
}
/// We use a private [InheritedWidget] to allow children to lookup the current
/// route if it was not explicitly passed to them.
class _UriRouteData extends InheritedWidget {
const _UriRouteData(this.route, {required super.child});
final PathRouteMatch route;
@override
bool updateShouldNotify(_UriRouteData oldWidget) {
return oldWidget.route != route;
}
}
typedef KeyBuilder = LocalKey Function(PathRouteMatch route);
class PathRoute {
/// A [PathRoute] which uses a unique key
PathRoute({
this.name,
required this.path,
required this.builder,
}) : parser = UriParser(UriTemplate(path)),
keyBuilder = (() {
final key = UniqueKey();
return (route) => key;
})();
/// A [PathRoute] which uses a static key
PathRoute.key({
this.name,
required this.path,
required this.builder,
required LocalKey key,
}) : parser = UriParser(UriTemplate(path)),
keyBuilder = ((route) => key);
/// A [PathRoute] which uses a dynamic key
PathRoute.keyBuilder(
{this.name,
required this.path,
required this.builder,
required KeyBuilder? keyBuilder})
: parser = UriParser(UriTemplate(path)),
keyBuilder = keyBuilder ??
(() {
final key = UniqueKey();
return (route) => key;
})();
/// The path used by this route, must be a valid [UriTemplate]
final String path;
/// A name to associate with the [Page] created when this matches
final String? name;
/// A [UriParser] used to match against this route's path
final UriPattern parser;
/// Returns a key for a given route, if the key matches the current page's
/// key, the content of the current page will be updated instead of
/// causing navigation to occur, allowing pages to share a [StatefulWidget]
/// across different paths.
final KeyBuilder keyBuilder;
/// Builds the [Widget] for pages matched by this route
final Widget Function(BuildContext context, PathRouteMatch route) builder;
}
class PathRouteInformationParser
extends RouteInformationParser<PathRouteMatch> {
const PathRouteInformationParser(
{required this.notFound, required this.routes});
/// The route to use when no route is matched
final PathRoute notFound;
/// A list of routes to match against
final List<PathRoute> routes;
@override
SynchronousFuture<PathRouteMatch> parseRouteInformation(
RouteInformation routeInformation) {
// Url to navigation state, we use a SynchronousFuture here because we need
// to be able to parse routes immediately during setup.
for (var route in routes) {
final match = route.parser.match(routeInformation.uri);
if (match != null && match.rest.path.isEmpty) {
return SynchronousFuture(PathRouteMatch(
uri: routeInformation.uri,
route: route,
parameters: match.parameters,
builder: route.builder,
extra: routeInformation.state));
}
}
return SynchronousFuture(PathRouteMatch(
uri: routeInformation.uri,
route: notFound,
parameters: {},
builder: notFound.builder,
extra: routeInformation.state));
}
@override
RouteInformation restoreRouteInformation(PathRouteMatch configuration) {
return RouteInformation(uri: configuration.uri, state: configuration.extra);
}
}
class PathRouteDelegate extends RouterDelegate<PathRouteMatch>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<PathRouteMatch> {
PathRouteDelegate({required this.initialRoute}) {
setNewRoutePath(initialRoute);
}
final PathRouteMatch initialRoute;
final List<Page> _pages = [];
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
// The navigator wants a unique array every time it builds, if we
// only pass the pages, it will not update
pages: [..._pages],
// Router complains if this isn't provided, just needs to remove the page from _pages.
onDidRemovePage: (page) {
_pages.remove(page);
},
);
}
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
ValueNotifier<PathRouteMatch>? _pageRoutes;
// we don't use an async function here because there's nothing async about
// this and we want to be able to complete the work from the constructor
// for our initial route
@override
Future<void> setNewRoutePath(PathRouteMatch configuration) {
final key = configuration.route.keyBuilder(configuration);
if (_pages.isNotEmpty && _pages.last.key == key) {
_pageRoutes!.value = configuration;
} else {
// Just because we have multiple routes doesn't mean we want it to result
// in multiple pages. For example in the case where we have a stateful
// navigation bar and /contacts and /contacts/1, we don't want
// a page transition when someone taps a contact.
//
// By using a ValueNotifier, we can update the content of a page when
// it's key matches the route
final routes = ValueNotifier<PathRouteMatch>(configuration);
_pageRoutes = routes;
// We only want a single level in our nav stack, so we'll clear the stack
// on nav. We could add to the end of the list if we want the stack to
// grow
if (_pages.isNotEmpty) {
_pages.clear();
}
_pages.add(MaterialPage(
maintainState: false,
key: key,
name: configuration.route.name,
child: ValueListenableBuilder<PathRouteMatch>(
valueListenable: routes,
builder: (context, current, _) => _UriRouteData(current,
child: current.builder(context, current)))));
notifyListeners();
}
return Future<void>(() {});
}
}
extension PathRouterExtension on BuildContext {
/// Navigate to a path
///
/// @param location The location ro redirect to
/// @param replace Whether to replace the path in the history
Future<void> go(String location,
{Object? extra, bool replace = false}) async {
final router = Router.of(this);
// Required to push new paths into the address bar on web
SystemNavigator.routeInformationUpdated(
uri: Uri.parse(location), state: extra, replace: replace);
final route = await router.routeInformationParser!.parseRouteInformation(
RouteInformation(uri: Uri.parse(location), state: extra));
// Report the route to the system so it doesn't get reverted (Router didChangeDependencies
// being invoked will cause it to revert to router.routeInformationProvider!.value).
router.routeInformationProvider!.routerReportsNewRouteInformation(
RouteInformation(uri: Uri.parse(location), state: extra));
router.routerDelegate.setNewRoutePath(route);
}
}
typedef PathRouteConfiguration = ({
PathRouteInformationParser routeInformationParser,
PathRouteDelegate routerDelegate
});
PathRouteConfiguration setupPathRouter(
{Uri? uri, required PathRoute notFound, required List<PathRoute> routes}) {
final parser = PathRouteInformationParser(routes: routes, notFound: notFound);
late final PathRouteMatch initialRoute;
parser
.parseRouteInformation(RouteInformation(uri: uri ?? Uri.parse("/")))
.then((value) {
initialRoute = value;
});
return (
routeInformationParser: parser,
routerDelegate: PathRouteDelegate(initialRoute: initialRoute)
);
}
import 'package:flutter/material.dart';
import 'bad_router.dart';
final routes = [
PathRoute(
name: "view_home",
path: "/",
builder: ((context, state) {
return Container(child: Text("Hello World"));
}),
PathRoute(
name: "view_about",
path: "/about",
builder: ((context, state) {
return Container(child: Text("About"));
}),
];
final notFound = PathRoute(
path: "404",
builder: (context, route) => Container(
alignment: Alignment.center,
child: Text("Not Found ${route.uri}")));
void main() {
final configuration = setupPathRouter(
notFound: notFound,
routes: routes);
runApp(MaterialApp.router(
routeInformationParser: configuration.routeInformationParser,
routerDelegate: configuration.routerDelegate,
));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment