blob: fed213cecafa2174b9e9c62def92460c8fcda4c1 [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 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'custom_transition_page.dart';
import 'go_route_information_parser.dart';
import 'go_route_match.dart';
import 'go_router_cupertino.dart';
import 'go_router_error_page.dart';
import 'go_router_material.dart';
import 'go_router_state.dart';
import 'logging.dart';
import 'route_data.dart';
import 'typedefs.dart';
/// GoRouter implementation of the RouterDelegate base class.
class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
with PopNavigatorRouterDelegateMixin<List<GoRouteMatch>>, ChangeNotifier {
/// Constructor for GoRouter's implementation of the
/// RouterDelegate base class.
GoRouterDelegate(
this._parser, {
required this.builderWithNav,
required this.errorPageBuilder,
required this.errorBuilder,
required this.observers,
required this.routerNeglect,
this.restorationScopeId,
});
// TODO(chunhtai): remove this once namedLocation is removed from go_router.
final GoRouteInformationParser _parser;
/// 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;
/// NavigatorObserver used to receive change notifications when
/// navigation changes.
final List<NavigatorObserver> observers;
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
/// Restoration ID to save and restore the state of the navigator, including
/// its history.
final String? restorationScopeId;
final GlobalKey<NavigatorState> _key = GlobalKey<NavigatorState>();
List<GoRouteMatch> _matches = const <GoRouteMatch>[];
/// Push the given location onto the page stack
void push(GoRouteMatch match) {
_matches.add(match);
notifyListeners();
}
/// Pop the top page off the GoRouter's page stack.
void pop() {
_matches.remove(_matches.last);
assert(_matches.isNotEmpty,
'have popped the last page off of the stack; there are no pages left to show');
notifyListeners();
}
/// For internal use; visible for testing only.
@visibleForTesting
List<GoRouteMatch> get matches => _matches;
/// For use by the Router architecture as part of the RouterDelegate.
@override
GlobalKey<NavigatorState> get navigatorKey => _key;
/// For use by the Router architecture as part of the RouterDelegate.
@override
List<GoRouteMatch> get currentConfiguration => _matches;
/// For use by the Router architecture as part of the RouterDelegate.
@override
Widget build(BuildContext context) => _builder(context, _matches);
/// For use by the Router architecture as part of the RouterDelegate.
@override
Future<void> setNewRoutePath(List<GoRouteMatch> configuration) {
_matches = configuration;
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null);
}
Widget _builder(BuildContext context, Iterable<GoRouteMatch> matches) {
List<Page<dynamic>>? pages;
Exception? error;
final String location = matches.last.fullUriString;
try {
// build the stack of pages
if (routerNeglect) {
Router.neglect(
context,
() => pages = getPages(context, matches.toList()).toList(),
);
} else {
pages = getPages(context, matches.toList()).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(
_parser,
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.length == 1 && matches.first.error != null) {
error = matches.first.error;
}
// wrap the returned Navigator to enable GoRouter.of(context).go()
final Uri uri = Uri.parse(location);
return builderWithNav(
context,
GoRouterState(
_parser,
location: location,
name: null, // no name available at the top level
// 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: _key, // 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, e.g.
/// 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<GoRouteMatch> matches,
) sync* {
assert(matches.isNotEmpty);
Map<String, String> params = <String, String>{};
for (final GoRouteMatch 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(
_parser,
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) =>
GoRouterMaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) {
assert(() {
log.info('CupertinoApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterCupertinoErrorScreen(s.error);
} else {
assert(() {
log.info('WidgetsApp found');
return true;
}());
_pageBuilderForAppType = pageBuilderForWidgetApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => GoRouterErrorScreen(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!,
);
}
}