blob: 37986714a3c340fe4408df1974541ac32c0b2972 [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 'builder.dart';
import 'configuration.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'typedefs.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,
}) : _configuration = configuration {
builder = RouteBuilder(
configuration: configuration,
builderWithNav: builderWithNav,
errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder,
restorationScopeId: restorationScopeId,
observers: observers,
onPopPageWithRouteMatch: _handlePopPageWithRouteMatch,
);
}
/// 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.didPop(result)) {
return false;
}
assert(match != null);
if (match is ImperativeRouteMatch) {
match.complete(result);
}
currentConfiguration = currentConfiguration.remove(match!);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
return true;
}());
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.
@override
Future<void> setNewRoutePath(RouteMatchList configuration) {
currentConfiguration = configuration;
assert(currentConfiguration.isNotEmpty || currentConfiguration.isError);
notifyListeners();
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
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;
}
}