blob: 49b6372c69335cf85102c2a24dfdf3f0333aae8d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/error_screen.dart';
import 'misc/errors.dart';
import 'pages/cupertino.dart';
import 'pages/custom_transition_page.dart';
import 'pages/material.dart';
import 'route.dart';
import 'route_data.dart';
import 'state.dart';
/// Signature of a go router builder function with navigator.
typedef GoRouterBuilderWithNav = Widget Function(
BuildContext context,
Widget child,
);
typedef _PageBuilderForAppType = Page<void> Function({
required LocalKey key,
required String? name,
required Object? arguments,
required String restorationId,
required Widget child,
});
typedef _ErrorBuilderForAppType = Widget Function(
BuildContext context,
GoRouterState state,
);
/// Signature for a function that takes in a `route` to be popped with
/// the `result` and returns a boolean decision on whether the pop
/// is successful.
///
/// The `match` is the corresponding [RouteMatch] the `route`
/// associates with.
///
/// Used by of [RouteBuilder.onPopPageWithRouteMatch].
typedef PopPageWithRouteMatchCallback = bool Function(
Route<dynamic> route, dynamic result, RouteMatchBase match);
/// Builds the top-level Navigator for GoRouter.
class RouteBuilder {
/// [RouteBuilder] constructor.
RouteBuilder({
required this.configuration,
required this.builderWithNav,
required this.errorPageBuilder,
required this.errorBuilder,
required this.restorationScopeId,
required this.observers,
required this.onPopPageWithRouteMatch,
this.requestFocus = true,
});
/// Builder function for a go router with Navigator.
final GoRouterBuilderWithNav builderWithNav;
/// Error page builder for the go router delegate.
final GoRouterPageBuilder? errorPageBuilder;
/// Error widget builder for the go router delegate.
final GoRouterWidgetBuilder? errorBuilder;
/// The route configuration for the app.
final RouteConfiguration configuration;
/// Restoration ID to save and restore the state of the navigator, including
/// its history.
final String? restorationScopeId;
/// Whether or not the navigator created by this builder and it's new topmost route should request focus
/// when the new route is pushed onto the navigator.
///
/// Defaults to true.
final bool requestFocus;
/// NavigatorObserver used to receive notifications when navigating in between routes.
/// changes.
final List<NavigatorObserver> observers;
/// A callback called when a `route` produced by `match` is about to be popped
/// with the `result`.
///
/// If this method returns true, this builder pops the `route` and `match`.
///
/// If this method returns false, this builder aborts the pop.
final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
/// Builds the top-level Navigator for the given [RouteMatchList].
Widget build(
BuildContext context,
RouteMatchList matchList,
bool routerNeglect,
) {
if (matchList.isEmpty && !matchList.isError) {
// The build method can be called before async redirect finishes. Build a
// empty box until then.
return const SizedBox.shrink();
}
assert(matchList.isError || !matchList.last.route.redirectOnly);
return builderWithNav(
context,
_CustomNavigator(
navigatorKey: configuration.navigatorKey,
observers: observers,
navigatorRestorationId: restorationScopeId,
onPopPageWithRouteMatch: onPopPageWithRouteMatch,
matchList: matchList,
matches: matchList.matches,
configuration: configuration,
errorBuilder: errorBuilder,
errorPageBuilder: errorPageBuilder,
),
);
}
}
class _CustomNavigator extends StatefulWidget {
const _CustomNavigator({
super.key,
required this.navigatorKey,
required this.observers,
required this.navigatorRestorationId,
required this.onPopPageWithRouteMatch,
required this.matchList,
required this.matches,
required this.configuration,
required this.errorBuilder,
required this.errorPageBuilder,
});
final GlobalKey<NavigatorState> navigatorKey;
final List<NavigatorObserver> observers;
/// The actual [RouteMatchBase]s to be built.
///
/// This can be different from matches in [matchList] if this widget is used
/// to build navigator in shell route. In this case, these matches come from
/// the [ShellRouteMatch.matches].
final List<RouteMatchBase> matches;
final RouteMatchList matchList;
final RouteConfiguration configuration;
final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
final String? navigatorRestorationId;
final GoRouterWidgetBuilder? errorBuilder;
final GoRouterPageBuilder? errorPageBuilder;
@override
State<StatefulWidget> createState() => _CustomNavigatorState();
}
class _CustomNavigatorState extends State<_CustomNavigator> {
HeroController? _controller;
late Map<Page<Object?>, RouteMatchBase> _pageToRouteMatchBase;
final GoRouterStateRegistry _registry = GoRouterStateRegistry();
List<Page<Object?>>? _pages;
@override
void didUpdateWidget(_CustomNavigator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.matchList != oldWidget.matchList) {
_pages = null;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Create a HeroController based on the app type.
if (_controller == null) {
if (isMaterialApp(context)) {
_controller = createMaterialHeroController();
} else if (isCupertinoApp(context)) {
_controller = createCupertinoHeroController();
} else {
_controller = HeroController();
}
}
// This method can also be called if any of the page builders depend on
// the context. In this case, make sure _pages are rebuilt.
_pages = null;
}
@override
void dispose() {
_controller?.dispose();
_registry.dispose();
super.dispose();
}
void _updatePages(BuildContext context) {
assert(_pages == null);
final List<Page<Object?>> pages = <Page<Object?>>[];
final Map<Page<Object?>, RouteMatchBase> pageToRouteMatchBase =
<Page<Object?>, RouteMatchBase>{};
final Map<Page<Object?>, GoRouterState> registry =
<Page<Object?>, GoRouterState>{};
if (widget.matchList.isError) {
pages.add(_buildErrorPage(context, widget.matchList));
} else {
for (final RouteMatchBase match in widget.matches) {
final Page<Object?>? page = _buildPage(context, match);
if (page == null) {
continue;
}
pages.add(page);
pageToRouteMatchBase[page] = match;
registry[page] =
match.buildState(widget.configuration, widget.matchList);
}
}
_pages = pages;
_registry.updateRegistry(registry);
_pageToRouteMatchBase = pageToRouteMatchBase;
}
Page<Object?>? _buildPage(BuildContext context, RouteMatchBase match) {
if (match is RouteMatch) {
if (match is ImperativeRouteMatch && match.matches.isError) {
return _buildErrorPage(context, match.matches);
}
return _buildPageForGoRoute(context, match);
}
if (match is ShellRouteMatch) {
return _buildPageForShellRoute(context, match);
}
throw GoError('unknown match type ${match.runtimeType}');
}
/// Builds a [Page] for a [RouteMatch]
Page<Object?>? _buildPageForGoRoute(BuildContext context, RouteMatch match) {
final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
final GoRouterState state =
match.buildState(widget.configuration, widget.matchList);
if (pageBuilder != null) {
final Page<Object?> page = pageBuilder(context, state);
if (page is! NoOpPage) {
return page;
}
}
final GoRouterWidgetBuilder? builder = match.route.builder;
if (builder == null) {
return null;
}
return _buildPlatformAdapterPage(context, state,
Builder(builder: (BuildContext context) {
return builder(context, state);
}));
}
/// Builds a [Page] for a [ShellRouteMatch]
Page<Object?> _buildPageForShellRoute(
BuildContext context,
ShellRouteMatch match,
) {
final GoRouterState state =
match.buildState(widget.configuration, widget.matchList);
final GlobalKey<NavigatorState> navigatorKey = match.navigatorKey;
final ShellRouteContext shellRouteContext = ShellRouteContext(
route: match.route,
routerState: state,
navigatorKey: navigatorKey,
routeMatchList: widget.matchList,
navigatorBuilder:
(List<NavigatorObserver>? observers, String? restorationScopeId) {
return _CustomNavigator(
// The state needs to persist across rebuild.
key: GlobalObjectKey(navigatorKey.hashCode),
navigatorRestorationId: restorationScopeId,
navigatorKey: navigatorKey,
matches: match.matches,
matchList: widget.matchList,
configuration: widget.configuration,
observers: observers ?? const <NavigatorObserver>[],
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
// This is used to recursively build pages under this shell route.
errorBuilder: widget.errorBuilder,
errorPageBuilder: widget.errorPageBuilder,
);
},
);
final Page<Object?>? page =
match.route.buildPage(context, state, shellRouteContext);
if (page != null && page is! NoOpPage) {
return page;
}
// Return the result of the route's builder() or pageBuilder()
return _buildPlatformAdapterPage(
context,
state,
Builder(
builder: (BuildContext context) {
return match.route.buildWidget(context, state, shellRouteContext)!;
},
),
);
}
_PageBuilderForAppType? _pageBuilderForAppType;
_ErrorBuilderForAppType? _errorBuilderForAppType;
void _cacheAppType(BuildContext context) {
// cache app type-specific page and error builders
if (_pageBuilderForAppType == null) {
assert(_errorBuilderForAppType == null);
// can be null during testing
final Element? elem = context is Element ? context : null;
if (elem != null && isMaterialApp(elem)) {
log('Using MaterialApp configuration');
_pageBuilderForAppType = pageBuilderForMaterialApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => MaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) {
log('Using CupertinoApp configuration');
_pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => CupertinoErrorScreen(s.error);
} else {
log('Using WidgetsApp configuration');
_pageBuilderForAppType = ({
required LocalKey key,
required String? name,
required Object? arguments,
required String restorationId,
required Widget child,
}) =>
NoTransitionPage<void>(
name: name,
arguments: arguments,
key: key,
restorationId: restorationId,
child: child,
);
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => ErrorScreen(s.error);
}
}
assert(_pageBuilderForAppType != null);
assert(_errorBuilderForAppType != null);
}
/// builds the page based on app type, i.e. MaterialApp vs. CupertinoApp
Page<Object?> _buildPlatformAdapterPage(
BuildContext context,
GoRouterState state,
Widget child,
) {
// build the page based on app type
_cacheAppType(context);
return _pageBuilderForAppType!(
key: state.pageKey,
name: state.name ?? state.path,
arguments: <String, String>{
...state.pathParameters,
...state.uri.queryParameters
},
restorationId: state.pageKey.value,
child: child,
);
}
GoRouterState _buildErrorState(RouteMatchList matchList) {
assert(matchList.isError);
return GoRouterState(
widget.configuration,
uri: matchList.uri,
matchedLocation: matchList.uri.path,
fullPath: matchList.fullPath,
pathParameters: matchList.pathParameters,
error: matchList.error,
pageKey: ValueKey<String>('${matchList.uri}(error)'),
topRoute: matchList.lastOrNull?.route,
);
}
/// Builds a an error page.
Page<void> _buildErrorPage(BuildContext context, RouteMatchList matchList) {
final GoRouterState state = _buildErrorState(matchList);
assert(state.error != null);
// If the error page builder is provided, use that, otherwise, if the error
// builder is provided, wrap that in an app-specific page (for example,
// MaterialPage). Finally, if nothing is provided, use a default error page
// wrapped in the app-specific page.
_cacheAppType(context);
final GoRouterWidgetBuilder? errorBuilder = widget.errorBuilder;
return widget.errorPageBuilder != null
? widget.errorPageBuilder!(context, state)
: _buildPlatformAdapterPage(
context,
state,
errorBuilder != null
? errorBuilder(context, state)
: _errorBuilderForAppType!(context, state),
);
}
bool _handlePopPage(Route<Object?> route, Object? result) {
final Page<Object?> page = route.settings as Page<Object?>;
final RouteMatchBase match = _pageToRouteMatchBase[page]!;
return widget.onPopPageWithRouteMatch(route, result, match);
}
@override
Widget build(BuildContext context) {
if (_pages == null) {
_updatePages(context);
}
assert(_pages != null);
return GoRouterStateRegistryScope(
registry: _registry,
child: HeroControllerScope(
controller: _controller!,
child: Navigator(
key: widget.navigatorKey,
restorationScopeId: widget.navigatorRestorationId,
pages: _pages!,
observers: widget.observers,
onPopPage: _handlePopPage,
),
),
);
}
}