blob: 8e9befe3555b9d0e9a43914f9797add8d431571e [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 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'builder.dart';
import 'configuration.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'route.dart';
/// GoRouter implementation of [RouterDelegate].
class GoRouterDelegate extends RouterDelegate<RouteMatchList>
with ChangeNotifier {
/// Constructor for GoRouter's implementation of the RouterDelegate base
/// class.
GoRouterDelegate({
required RouteConfiguration configuration,
required GoRouterBuilderWithNav builderWithNav,
required GoRouterPageBuilder? errorPageBuilder,
required GoRouterWidgetBuilder? errorBuilder,
required List<NavigatorObserver> observers,
required this.routerNeglect,
String? restorationScopeId,
bool requestFocus = true,
}) : _configuration = configuration {
builder = RouteBuilder(
configuration: configuration,
builderWithNav: builderWithNav,
errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder,
restorationScopeId: restorationScopeId,
observers: observers,
onPopPageWithRouteMatch: _handlePopPageWithRouteMatch,
requestFocus: requestFocus,
);
}
/// Builds the top-level Navigator given a configuration and location.
@visibleForTesting
late final RouteBuilder builder;
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
final RouteConfiguration _configuration;
_NavigatorStateIterator _createNavigatorStateIterator() =>
_NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!);
@override
Future<bool> popRoute() async {
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
while (iterator.moveNext()) {
final bool didPop = await iterator.current.maybePop();
if (didPop) {
return true;
}
}
return false;
}
/// Returns `true` if the active Navigator can pop.
bool canPop() {
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
while (iterator.moveNext()) {
if (iterator.current.canPop()) {
return true;
}
}
return false;
}
/// Pops the top-most route.
void pop<T extends Object?>([T? result]) {
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
while (iterator.moveNext()) {
if (iterator.current.canPop()) {
iterator.current.pop<T>(result);
return;
}
}
throw GoError('There is nothing to pop');
}
void _debugAssertMatchListNotEmpty() {
assert(
currentConfiguration.isNotEmpty,
'You have popped the last page off of the stack,'
' there are no pages left to show',
);
}
bool _handlePopPageWithRouteMatch(
Route<Object?> route, Object? result, RouteMatch? match) {
if (route.willHandlePopInternally) {
final bool popped = route.didPop(result);
assert(!popped);
return popped;
}
assert(match != null);
final RouteBase routeBase = match!.route;
if (routeBase is! GoRoute || routeBase.onExit == null) {
route.didPop(result);
_completeRouteMatch(result, match);
return true;
}
// The _handlePopPageWithRouteMatch is called during draw frame, schedule
// a microtask in case the onExit callback want to launch dialog or other
// navigator operations.
scheduleMicrotask(() async {
final bool onExitResult =
await routeBase.onExit!(navigatorKey.currentContext!);
if (onExitResult) {
_completeRouteMatch(result, match);
}
});
return false;
}
void _completeRouteMatch(Object? result, RouteMatch match) {
if (match is ImperativeRouteMatch) {
match.complete(result);
}
currentConfiguration = currentConfiguration.remove(match);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
return true;
}());
}
/// For use by the Router architecture as part of the RouterDelegate.
GlobalKey<NavigatorState> get navigatorKey => _configuration.navigatorKey;
/// For use by the Router architecture as part of the RouterDelegate.
@override
RouteMatchList currentConfiguration = RouteMatchList.empty;
/// For use by the Router architecture as part of the RouterDelegate.
@override
Widget build(BuildContext context) {
return builder.build(
context,
currentConfiguration,
routerNeglect,
);
}
/// For use by the Router architecture as part of the RouterDelegate.
// This class avoids using async to make sure the route is processed
// synchronously if possible.
@override
Future<void> setNewRoutePath(RouteMatchList configuration) {
if (currentConfiguration == configuration) {
return SynchronousFuture<void>(null);
}
assert(configuration.isNotEmpty || configuration.isError);
final BuildContext? navigatorContext = navigatorKey.currentContext;
// If navigator is not built or disposed, the GoRoute.onExit is irrelevant.
if (navigatorContext != null) {
final int compareUntil = math.min(
currentConfiguration.matches.length,
configuration.matches.length,
);
int indexOfFirstDiff = 0;
for (; indexOfFirstDiff < compareUntil; indexOfFirstDiff++) {
if (currentConfiguration.matches[indexOfFirstDiff] !=
configuration.matches[indexOfFirstDiff]) {
break;
}
}
if (indexOfFirstDiff < currentConfiguration.matches.length) {
final List<GoRoute> exitingGoRoutes = currentConfiguration.matches
.sublist(indexOfFirstDiff)
.map<RouteBase>((RouteMatch match) => match.route)
.whereType<GoRoute>()
.toList();
return _callOnExitStartsAt(exitingGoRoutes.length - 1,
navigatorContext: navigatorContext, routes: exitingGoRoutes)
.then<void>((bool exit) {
if (!exit) {
return SynchronousFuture<void>(null);
}
return _setCurrentConfiguration(configuration);
});
}
}
return _setCurrentConfiguration(configuration);
}
/// Calls [GoRoute.onExit] starting from the index
///
/// The returned future resolves to true if all routes below the index all
/// return true. Otherwise, the returned future resolves to false.
static Future<bool> _callOnExitStartsAt(int index,
{required BuildContext navigatorContext, required List<GoRoute> routes}) {
if (index < 0) {
return SynchronousFuture<bool>(true);
}
final GoRoute goRoute = routes[index];
if (goRoute.onExit == null) {
return _callOnExitStartsAt(index - 1,
navigatorContext: navigatorContext, routes: routes);
}
Future<bool> handleOnExitResult(bool exit) {
if (exit) {
return _callOnExitStartsAt(index - 1,
navigatorContext: navigatorContext, routes: routes);
}
return SynchronousFuture<bool>(false);
}
final FutureOr<bool> exitFuture = goRoute.onExit!(navigatorContext);
if (exitFuture is bool) {
return handleOnExitResult(exitFuture);
}
return exitFuture.then<bool>(handleOnExitResult);
}
Future<void> _setCurrentConfiguration(RouteMatchList configuration) {
currentConfiguration = configuration;
notifyListeners();
return SynchronousFuture<void>(null);
}
}
/// An iterator that iterates through navigators that [GoRouterDelegate]
/// created from the inner to outer.
///
/// The iterator starts with the navigator that hosts the top-most route. This
/// navigator may not be the inner-most navigator if the top-most route is a
/// pageless route, such as a dialog or bottom sheet.
class _NavigatorStateIterator extends Iterator<NavigatorState> {
_NavigatorStateIterator(this.matchList, this.root)
: index = matchList.matches.length - 1;
final RouteMatchList matchList;
int index;
final NavigatorState root;
@override
late NavigatorState current;
RouteBase _getRouteAtIndex(int index) => matchList.matches[index].route;
void _findsNextIndex() {
final GlobalKey<NavigatorState>? parentNavigatorKey =
_getRouteAtIndex(index).parentNavigatorKey;
if (parentNavigatorKey == null) {
index -= 1;
return;
}
for (index -= 1; index >= 0; index -= 1) {
final RouteBase route = _getRouteAtIndex(index);
if (route is ShellRouteBase) {
if (route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)) ==
parentNavigatorKey) {
return;
}
}
}
assert(root == parentNavigatorKey.currentState);
}
@override
bool moveNext() {
if (index < 0) {
return false;
}
_findsNextIndex();
while (index >= 0) {
final RouteBase route = _getRouteAtIndex(index);
if (route is ShellRouteBase) {
final GlobalKey<NavigatorState> navigatorKey =
route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1));
// Must have a ModalRoute parent because the navigator ShellRoute
// created must not be the root navigator.
final ModalRoute<Object?> parentModalRoute =
ModalRoute.of(navigatorKey.currentContext!)!;
// There may be pageless route on top of ModalRoute that the
// parentNavigatorKey is in. For example an open dialog.
if (parentModalRoute.isCurrent) {
current = navigatorKey.currentState!;
return true;
}
}
_findsNextIndex();
}
assert(index == -1);
current = root;
return true;
}
}