Last active
October 4, 2024 08:56
-
-
Save jezell/0aa4c66990d17d342efedb19b7621381 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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