blob: 28ef9b76a78e695a6dce5c66be30e6be36ea0141 [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 'matching.dart';
import 'misc/error_screen.dart';
import 'pages/cupertino.dart';
import 'pages/custom_transition_page.dart';
import 'pages/material.dart';
import 'typed_routing.dart';
import 'typedefs.dart';
/// 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,
});
/// 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;
/// NavigatorObserver used to receive notifications when navigating in between routes.
/// changes.
final List<NavigatorObserver> observers;
/// Builds the top-level Navigator by invoking the build method on each
/// matching route
Widget build(
BuildContext context,
RouteMatchList matches,
VoidCallback pop,
Key navigatorKey,
bool routerNeglect,
) {
List<Page<dynamic>>? pages;
Exception? error;
final String location = matches.location.toString();
final List<RouteMatch> matchesList = matches.matches;
try {
// build the stack of pages
if (routerNeglect) {
Router.neglect(
context,
() => pages = getPages(context, matchesList).toList(),
);
} else {
pages = getPages(context, matchesList).toList();
}
// note that we need to catch it this way to get all the info, e.g. the
// file/line info for an error in an inline function impl, e.g. an inline
// `redirect` impl
// ignore: avoid_catches_without_on_clauses
} catch (err, stack) {
assert(() {
log.severe('Exception during GoRouter navigation', err, stack);
return true;
}());
// if there's an error, show an error page
error = err is Exception ? err : Exception(err);
final Uri uri = Uri.parse(location);
pages = <Page<dynamic>>[
_errorPageBuilder(
context,
GoRouterState(
configuration,
location: location,
subloc: uri.path,
name: null,
queryParams: uri.queryParameters,
error: error,
),
),
];
}
// we should've set pages to something by now
assert(pages != null);
// pass either the match error or the build error along to the navigator
// builder, preferring the match error
if (matches.isError) {
error = matches.error;
}
// wrap the returned Navigator to enable GoRouter.of(context).go()
final Uri uri = Uri.parse(location);
return builderWithNav(
context,
GoRouterState(
configuration,
location: location,
// no name available at the top level
name: null,
// trim the query params off the subloc to match route.redirect
subloc: uri.path,
// pass along the query params 'cuz that's all we have right now
queryParams: uri.queryParameters,
// pass along the error, if there is one
error: error,
),
Navigator(
restorationScopeId: restorationScopeId,
key: navigatorKey,
// needed to enable Android system Back button
pages: pages!,
observers: observers,
onPopPage: (Route<dynamic> route, dynamic result) {
if (!route.didPop(result)) {
return false;
}
pop();
return true;
},
),
);
}
/// Get the stack of sub-routes that matches the location and turn it into a
/// stack of pages, for example:
///
/// routes: <GoRoute>[
/// /
/// family/:fid
/// person/:pid
/// /login
/// ]
///
/// loc: /
/// pages: [ HomePage()]
///
/// loc: /login
/// pages: [ LoginPage() ]
///
/// loc: /family/f2
/// pages: [ HomePage(), FamilyPage(f2) ]
///
/// loc: /family/f2/person/p1
/// pages: [ HomePage(), FamilyPage(f2), PersonPage(f2, p1) ]
@visibleForTesting
Iterable<Page<dynamic>> getPages(
BuildContext context,
List<RouteMatch> matches,
) sync* {
assert(matches.isNotEmpty);
Map<String, String> params = <String, String>{};
for (final RouteMatch match in matches) {
// merge new params to keep params from previously matched paths, e.g.
// /family/:fid/person/:pid provides fid and pid to person/:pid
params = <String, String>{...params, ...match.decodedParams};
// get a page from the builder and associate it with a sub-location
final GoRouterState state = GoRouterState(
configuration,
location: match.fullUriString,
subloc: match.subloc,
name: match.route.name,
path: match.route.path,
fullpath: match.fullpath,
params: params,
error: match.error,
queryParams: match.queryParams,
extra: match.extra,
pageKey: match.pageKey, // push() remaps the page key for uniqueness
);
if (match.error != null) {
yield _errorPageBuilder(context, state);
break;
}
final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
Page<dynamic>? page;
if (pageBuilder != null) {
page = pageBuilder(context, state);
if (page is NoOpPage) {
page = null;
}
}
yield page ?? _pageBuilder(context, state, match.route.builder);
}
}
Page<void> Function({
required LocalKey key,
required String? name,
required Object? arguments,
required String restorationId,
required Widget child,
})? _pageBuilderForAppType;
Widget Function(
BuildContext context,
GoRouterState state,
)? _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)) {
assert(() {
log.info('MaterialApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForMaterialApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => MaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) {
assert(() {
log.info('CupertinoApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => CupertinoErrorScreen(s.error);
} else {
assert(() {
log.info('WidgetsApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForWidgetApp;
_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<dynamic> _pageBuilder(
BuildContext context,
GoRouterState state,
GoRouterWidgetBuilder builder,
) {
// build the page based on app type
_cacheAppType(context);
return _pageBuilderForAppType!(
key: state.pageKey,
name: state.name ?? state.fullpath,
arguments: <String, String>{...state.params, ...state.queryParams},
restorationId: state.pageKey.value,
child: builder(context, state),
);
}
/// Builds a page without any transitions.
Page<void> pageBuilderForWidgetApp({
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,
);
Page<void> _errorPageBuilder(
BuildContext context,
GoRouterState state,
) {
// if the error page builder is provided, use that; otherwise, if the error
// builder is provided, wrap that in an app-specific page, e.g.
// MaterialPage; finally, if nothing is provided, use a default error page
// wrapped in the app-specific page, e.g.
// MaterialPage(GoRouterMaterialErrorPage(...))
_cacheAppType(context);
return errorPageBuilder != null
? errorPageBuilder!(context, state)
: _pageBuilder(
context,
state,
errorBuilder ?? _errorBuilderForAppType!,
);
}
}