blob: abf6329887fde5f60a6a4baa175fb9cf1c0167b7 [file] [log] [blame]
// Copyright 2014 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:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
///
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case should only happen in a web application where the [Router]
/// reports route change back to web engine.
class RouteInformation {
/// Creates a route information.
const RouteInformation({this.location, this.state});
/// The location of the application.
///
/// The string is usually in the format of multiple string identifiers with
/// slashes in between. ex: `/`, `/path`, `/path/to/the/app`.
///
/// It is equivalent to the URL in a web application.
final String? location;
/// The state of the application in the [location].
///
/// The app can have different states even in the same location. For example
/// the text inside a [TextField] or the scroll position in a [ScrollView],
/// these widget states can be stored in the [state].
///
/// It's only used in the web application currently. In a web application,
/// this property is stored into browser history entry when the [Router]
/// report this route information back to the web engine through the
/// [PlatformRouteInformationProvider], so we can get the url along with state
/// back when the user click the forward or backward buttons.
///
/// The state must be serializable.
final Object? state;
}
/// The dispatcher for opening and closing pages of an application.
///
/// This widget listens for routing information from the operating system (e.g.
/// an initial route provided on app startup, a new route obtained when an
/// intent is received, or a notification that the user hit the system back
/// button), parses route information into data of type `T`, and then converts
/// that data into [Page] objects that it passes to a [Navigator].
///
/// Additionally, every single part of that previous sentence can be overridden
/// and configured as desired.
///
/// The [routeInformationProvider] can be overridden to change how the name of
/// the route is obtained. the [RouteInformationProvider.value] when the
/// [Router] is first created is used as the initial route, and subsequent
/// notifications from the [RouteInformationProvider] to its listeners are
/// treated as notifications that the route information has changed.
///
/// The [backButtonDispatcher] can be overridden to change how back button
/// notifications are received. This must be a [BackButtonDispatcher], which is
/// an object where callbacks can be registered, and which can be chained
/// so that back button presses are delegated to subsidiary routers. The
/// callbacks are invoked to indicate that the user is trying to close the
/// current route (by pressing the system back button); the [Router] ensures
/// that when this callback is invoked, the message is passed to the
/// [routerDelegate] and its result is provided back to the
/// [backButtonDispatcher]. Some platforms don't have back buttons and on those
/// platforms it is completely normal that this notification is never sent. The
/// common [backButtonDispatcher] for root router is an instance of
/// [RootBackButtonDispatcher], which uses a [WidgetsBindingObserver] to listen
/// to the `popRoute` notifications from [SystemChannels.navigation]. A
/// common alternative is [ChildBackButtonDispatcher], which must be provided
/// the [BackButtonDispatcher] of its ancestor [Router] (available via
/// [Router.of]).
///
/// The [routeInformationParser] can be overridden to change how names obtained
/// from the [routeInformationProvider] are interpreted. It must implement the
/// [RouteInformationParser] interface, specialized with the same type as the
/// [Router] itself. This type, `T`, represents the data type that the
/// [routeInformationParser] will generate.
///
/// The [routerDelegate] can be overridden to change how the output of the
/// [routeInformationParser] is interpreted. It must implement the
/// [RouterDelegate] interface, also specialized with `T`; it takes as input
/// the data (of type `T`) from the [routeInformationParser], and is responsible
/// for providing a navigating widget to insert into the widget tree. The
/// [RouterDelegate] interface is also [Listenable]; notifications are taken
/// to mean that the [Router] needs to rebuild.
///
/// ## Concerns regarding asynchrony
///
/// Some of the APIs (notably those involving [RouteInformationParser] and
/// [RouterDelegate]) are asynchronous.
///
/// When developing objects implementing these APIs, if the work can be done
/// entirely synchronously, then consider using [SynchronousFuture] for the
/// future returned from the relevant methods. This will allow the [Router] to
/// proceed in a completely synchronous way, which removes a number of
/// complications.
///
/// Using asynchronous computation is entirely reasonable, however, and the API
/// is designed to support it. For example, maybe a set of images need to be
/// loaded before a route can be shown; waiting for those images to be loaded
/// before [RouterDelegate.setNewRoutePath] returns is a reasonable approach to
/// handle this case.
///
/// If an asynchronous operation is ongoing when a new one is to be started, the
/// precise behavior will depend on the exact circumstances, as follows:
///
/// If the active operation is a [routeInformationParser] parsing a new route information:
/// that operation's result, if it ever completes, will be discarded.
///
/// If the active operation is a [routerDelegate] handling a pop request:
/// the previous pop is immediately completed with "false", claiming that the
/// previous pop was not handled (this may cause the application to close).
///
/// If the active operation is a [routerDelegate] handling an initial route
/// or a pushed route, the result depends on the new operation. If the new
/// operation is a pop request, then the original operation's result, if it ever
/// completes, will be discarded. If the new operation is a push request,
/// however, the [routeInformationParser] will be requested to start the parsing, and
/// only if that finishes before the original [routerDelegate] request
/// completes will that original request's result be discarded.
///
/// If the identity of the [Router] widget's delegates change while an
/// asynchronous operation is in progress, to keep matters simple, all active
/// asynchronous operations will have their results discarded. It is generally
/// considered unusual for these delegates to change during the lifetime of the
/// [Router].
///
/// If the [Router] itself is disposed while an an asynchronous operation is in
/// progress, all active asynchronous operations will have their results
/// discarded also.
///
/// No explicit signals are provided to the [routeInformationParser] or
/// [routerDelegate] to indicate when any of the above happens, so it is
/// strongly recommended that [RouteInformationParser] and [RouterDelegate]
/// implementations not perform extensive computation.
///
/// ## Application architectural design
///
/// An application can have zero, one, or many [Router] widgets, depending on
/// its needs.
///
/// An application might have no [Router] widgets if it has only one "screen",
/// or if the facilities provided by [Navigator] are sufficient.
///
/// A particularly elaborate application might have multiple [Router] widgets,
/// in a tree configuration, with the first handling the entire route parsing
/// and making the result available for routers in the subtree. The routers in
/// the subtree do not participate in route information parsing but merely take the
/// result from the first router to build their sub routes.
///
/// Most applications only need a single [Router].
///
/// ## URL updates for web applications
///
/// In the web platform, it is important to keeps the URL up to date with the
/// app state. This ensures the browser constructs its history entry
/// correctly so that its forward and backward buttons continue to work.
///
/// If the [routeInformationProvider] is a [PlatformRouteInformationProvider]
/// and a app state change leads to [Router] rebuilds, the [Router] will detect
/// such a event and retrieve the new route information from the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation]. If the location in the
/// new route information is different from the current location, the router
/// sends the new route information to the engine through the
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation].
///
/// By Providing implementations of these two methods in the subclasses and using
/// the [PlatformRouteInformationProvider], you can enable the [Router] widget to
/// update the URL in the browser automatically.
///
/// You can force the [Router] to report the new route information back to the
/// engine even if the [RouteInformation.location] has not changed. By calling
/// the [Router.navigate], the [Router] will be forced to report the route
/// information back to the engine after running the callback. This is useful
/// when you want to support the browser backward and forward buttons without
/// changing the URL. For example, the scroll position of a scroll view may be
/// saved in the [RouteInformation.state]. If you use the [Router.navigate] to
/// update the scroll position, the browser will create a new history entry with
/// the [RouteInformation.state] that stores the new scroll position. when the
/// users click the backward button, the browser will go back to previous scroll
/// position without changing the url bar.
///
/// You can also force the [Router] to ignore a one time route information
/// update by providing a one time app state update in a callback and pass it
/// into the [Router.neglect]. The [Router] will not report any route
/// information even if it detects location change as a result of running the
/// callback. This is particularly useful when you don't want the browser to
/// create a browser history entry for this app state update.
///
/// You can also choose to opt out of URL updates entirely. Simply ignore the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] without providing the
/// implementations will prevent the [Router] from reporting the URL back to the
/// web engine. This is not recommended in general, but You may decide to opt
/// out in these cases:
///
/// * If you are not writing a web application.
///
/// * If you have multiple router widgets in your app, then only one router
/// widget should update the URL (Usually the top-most one created by the
/// [WidgetsApp.router]/[MaterialApp.router]/[CupertinoApp.router]).
///
/// * If your app does not care about the in-app navigation using the browser's
/// forward and backward buttons.
///
/// Otherwise, we strongly recommend implementing the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] to provide optimal
/// user experience in the web application.
class Router<T> extends StatefulWidget {
/// Creates a router.
///
/// The [routeInformationProvider] and [routeInformationParser] can be null if this
/// router does not depend on route information. A common example is a sub router
/// that builds its content completely relies on the app state.
///
/// If the [routeInformationProvider] is not null, the [routeInformationParser] must
/// also not be null.
///
/// The [routerDelegate] must not be null.
const Router({
Key? key,
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
}) : assert(
(routeInformationProvider == null) == (routeInformationParser == null),
'You must provide both routeInformationProvider and routeInformationParser '
'if this router parses route information. Otheriwse, they should both '
'be null.'
),
assert(routerDelegate != null),
super(key: key);
/// The route information provider for the router.
///
/// The value at the time of first build will be used as the initial route.
/// The [Router] listens to this provider and rebuilds with new names when
/// it notifies.
///
/// This can be null if this router does not rely on the route information
/// to build its content. In such case, the [routeInformationParser] can also be
/// null.
final RouteInformationProvider? routeInformationProvider;
/// The route information parser for the router.
///
/// When the [Router] gets a new route information from the [routeInformationProvider],
/// the [Router] uses this delegate to parse the route information and produce a
/// configuration. The configuration will be used by [routerDelegate] and
/// eventually rebuilds the [Router] widget.
///
/// Since this delegate is the primary consumer of the [routeInformationProvider],
/// it must not be null if [routeInformationProvider] is not null.
final RouteInformationParser<T>? routeInformationParser;
/// The router delegate for the router.
///
/// This delegate consumes the configuration from [routeInformationParser] and
/// builds a navigating widget for the [Router].
///
/// It is also the primary respondent for the [backButtonDispatcher]. The
/// [Router] relies on the [RouterDelegate.popRoute] to handles the back
/// button intends.
///
/// If the [RouterDelegate.currentConfiguration] returns a non-null object,
/// this [Router] will opt for URL updates.
final RouterDelegate<T> routerDelegate;
/// The back button dispatcher for the router.
///
/// The two common alternatives are the [RootBackButtonDispatcher] for root
/// router, or the [ChildBackButtonDispatcher] for other routers.
final BackButtonDispatcher? backButtonDispatcher;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// Use this method when you need to access the delegates in the [Router].
/// For example, you need to access the [backButtonDispatcher] of the parent
/// router to create a [ChildBackButtonDispatcher] for a nested router.
/// Another use case may be updating the value in [routeInformationProvider]
/// to navigate to a new route.
static Router<dynamic>? of(BuildContext context, {bool nullOk = false}) {
final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
assert(() {
if (scope == null && !nullOk) {
throw FlutterError(
'Router operation requested with a context that does not include a Router.\n'
'The context used to retrieve the Router must be that of a widget that '
'is a descendant of a Router widget.'
);
}
return true;
}());
return scope?.routerState.widget;
}
/// Forces the [Router] to run the [callback] and reports the route
/// information back to the engine.
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will only report
/// them if it detects the [RouteInformation.location] changes. Use this
/// method if you want the [Router] to report the route information even if
/// the location does not change. This can be useful when you want to
/// support the browser backward and forward button without changing the URL.
///
/// For example, you can store certain state such as the scroll position into
/// the [RouteInformation.state]. If you use this method to update the
/// scroll position multiple times with the same URL, the browser will create
/// a stack of new history entries with the same URL but different
/// [RouteInformation.state]s that store the new scroll positions. If the user
/// click the backward button in the browser, the browser will restore the
/// scroll positions saved in history entries without changing the URL.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [neglect]: which forces the [Router] to not report the route
/// information even if location does change.
static void navigate(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()!
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.must, callback);
}
/// Forces the [Router] to to run the [callback] without reporting the route
/// information back to the engine.
///
/// Use this method if you don't want the [Router] to report the new route
/// information even if it detects changes as a result of running the
/// [callback].
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will report them
/// automatically if it detects the [RouteInformation.location] changes. You
/// can use this method if you want to navigate to a new route without
/// creating the browser history entry.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [navigate]: which forces the [Router] to report the route information
/// even if location does not change.
static void neglect(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()!
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.ignore, callback);
}
@override
State<Router<T>> createState() => _RouterState<T>();
}
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
// Whether to report the route information in this build cycle.
enum _IntentionToReportRouteInformation {
// We haven't receive any signal on whether to report.
none,
// Report if route information changes.
maybe,
// Report regardless of route information changes.
must,
// Don't report regardless of route information changes.
ignore,
}
class _RouterState<T> extends State<Router<T>> {
Object? _currentRouteInformationParserTransaction;
Object? _currentRouterDelegateTransaction;
late _IntentionToReportRouteInformation _currentIntentionToReport;
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
if (widget.routeInformationProvider != null) {
_processInitialRoute();
}
}
bool _routeInformationReportingTaskScheduled = false;
String? _lastSeenLocation;
void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled)
return;
assert(_currentIntentionToReport != _IntentionToReportRouteInformation.none);
_routeInformationReportingTaskScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback(_reportRouteInformation);
}
void _reportRouteInformation(Duration timestamp) {
assert(_routeInformationReportingTaskScheduled);
_routeInformationReportingTaskScheduled = false;
switch (_currentIntentionToReport) {
case _IntentionToReportRouteInformation.none:
assert(false);
return;
case _IntentionToReportRouteInformation.ignore:
// In the ignore case, we still want to update the _lastSeenLocation.
final RouteInformation? routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
_lastSeenLocation = routeInformation.location;
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
case _IntentionToReportRouteInformation.maybe:
final RouteInformation? routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
if (_lastSeenLocation != routeInformation.location) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(routeInformation);
_lastSeenLocation = routeInformation.location;
}
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
case _IntentionToReportRouteInformation.must:
final RouteInformation? routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(routeInformation);
_lastSeenLocation = routeInformation.location;
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
}
}
RouteInformation? _retrieveNewRouteInformation() {
final T? configuration = widget.routerDelegate.currentConfiguration;
if (configuration == null)
return null;
final RouteInformation? routeInformation = widget.routeInformationParser!.restoreRouteInformation(configuration);
assert((){
if (routeInformation == null) {
FlutterError.reportError(
const FlutterErrorDetails(
exception:
'Router.routeInformationParser returns a null RouteInformation. '
'If you opt for route information reporting, the '
'routeInformationParser must not report null for a given '
'configuration.'
),
);
}
return true;
}());
return routeInformation;
}
void _setStateWithExplicitReportStatus(
_IntentionToReportRouteInformation status,
VoidCallback fn,
) {
assert(status != null);
assert(status.index >= _IntentionToReportRouteInformation.must.index);
assert(() {
if (_currentIntentionToReport.index >= _IntentionToReportRouteInformation.must.index &&
_currentIntentionToReport != status) {
FlutterError.reportError(
const FlutterErrorDetails(
exception:
'Both Router.navigate and Router.neglect have been called in this '
'build cycle, and the Router cannot decide whether to report the '
'route information. Please make sure only one of them is called '
'within the same build cycle.'
),
);
}
return true;
}());
_currentIntentionToReport = status;
_scheduleRouteInformationReportingTask();
fn();
}
void _maybeNeedToReportRouteInformation() {
_currentIntentionToReport = _currentIntentionToReport != _IntentionToReportRouteInformation.none
? _currentIntentionToReport
: _IntentionToReportRouteInformation.maybe;
_scheduleRouteInformationReportingTask();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_maybeNeedToReportRouteInformation();
}
@override
void didUpdateWidget(Router<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.routeInformationProvider != oldWidget.routeInformationProvider ||
widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
widget.routeInformationParser != oldWidget.routeInformationParser ||
widget.routerDelegate != oldWidget.routerDelegate) {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
}
if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
if (oldWidget.routeInformationProvider?.value != widget.routeInformationProvider?.value) {
_handleRouteInformationProviderNotification();
}
}
if (widget.backButtonDispatcher != oldWidget.backButtonDispatcher) {
oldWidget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
}
if (widget.routerDelegate != oldWidget.routerDelegate) {
oldWidget.routerDelegate.removeListener(_handleRouterDelegateNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_maybeNeedToReportRouteInformation();
}
}
@override
void dispose() {
widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
_currentRouteInformationParserTransaction = null;
_currentRouterDelegateTransaction = null;
super.dispose();
}
void _processInitialRoute() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
_lastSeenLocation = widget.routeInformationProvider!.value!.location;
widget.routeInformationParser!
.parseRouteInformation(widget.routeInformationProvider!.value!)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setInitialRoutePath)
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
}
void _handleRouteInformationProviderNotification() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
_lastSeenLocation = widget.routeInformationProvider!.value!.location;
widget.routeInformationParser!
.parseRouteInformation(widget.routeInformationProvider!.value!)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setNewRoutePath)
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
}
Future<bool> _handleBackButtonDispatcherNotification() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
return widget.routerDelegate
.popRoute()
.then<bool>(_verifyRouterDelegatePopStillCurrent(_currentRouterDelegateTransaction, widget))
.then<bool>((bool data) {
_rebuild();
_maybeNeedToReportRouteInformation();
return SynchronousFuture<bool>(data);
});
}
static final Future<dynamic> _never = Completer<dynamic>().future; // won't ever complete
_AsyncPassthrough<T> _verifyRouteInformationParserStillCurrent(Object? transaction, Router<T> originalWidget) {
return (T data) {
if (transaction == _currentRouteInformationParserTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate) {
return SynchronousFuture<T>(data);
}
return _never as Future<T>;
};
}
_AsyncPassthrough<void> _verifyRouterDelegatePushStillCurrent(Object? transaction, Router<T> originalWidget) {
return (void data) {
if (transaction == _currentRouterDelegateTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate)
return SynchronousFuture<void>(data);
return _never;
};
}
_AsyncPassthrough<bool> _verifyRouterDelegatePopStillCurrent(Object? transaction, Router<T> originalWidget) {
return (bool data) {
if (transaction == _currentRouterDelegateTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate) {
return SynchronousFuture<bool>(data);
}
// A rebuilt was trigger from a different source. Returns true to
// prevent bubbling.
return SynchronousFuture<bool>(true);
};
}
Future<void> _rebuild([void value]) {
setState(() {/* routerDelegate is ready to rebuild */});
return SynchronousFuture<void>(value);
}
void _handleRouterDelegateNotification() {
setState(() {/* routerDelegate wants to rebuild */});
_maybeNeedToReportRouteInformation();
}
@override
Widget build(BuildContext context) {
return _RouterScope(
routeInformationProvider: widget.routeInformationProvider,
backButtonDispatcher: widget.backButtonDispatcher,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
routerState: this,
child: Builder(
// We use a Builder so that the build method below
// will have a BuildContext that contains the _RouterScope.
builder: widget.routerDelegate.build,
),
);
}
}
class _RouterScope extends InheritedWidget {
const _RouterScope({
Key? key,
required this.routeInformationProvider,
required this.backButtonDispatcher,
required this.routeInformationParser,
required this.routerDelegate,
required this.routerState,
required Widget child,
}) : assert(routeInformationProvider == null || routeInformationParser != null),
assert(routerDelegate != null),
assert(routerState != null),
super(key: key, child: child);
final ValueListenable<RouteInformation?>? routeInformationProvider;
final BackButtonDispatcher? backButtonDispatcher;
final RouteInformationParser<dynamic>? routeInformationParser;
final RouterDelegate<dynamic> routerDelegate;
final _RouterState<dynamic> routerState;
@override
bool updateShouldNotify(_RouterScope oldWidget) {
return routeInformationProvider != oldWidget.routeInformationProvider ||
backButtonDispatcher != oldWidget.backButtonDispatcher ||
routeInformationParser != oldWidget.routeInformationParser ||
routerDelegate != oldWidget.routerDelegate ||
routerState != oldWidget.routerState;
}
}
/// A class that can be extended or mixed in that invokes a single callback,
/// which then returns a value.
///
/// While multiple callbacks can be registered, when a notification is
/// dispatched there must be only a single callback. The return values of
/// multiple callbacks are not aggregated.
///
/// `T` is the return value expected from the callback.
///
/// See also:
///
/// * [Listenable] and its subclasses, which provide a similar mechanism for
/// one-way signalling.
class _CallbackHookProvider<T> {
final ObserverList<ValueGetter<T>> _callbacks = ObserverList<ValueGetter<T>>();
/// Whether a callback is currently registered.
@protected
bool get hasCallbacks => _callbacks.isNotEmpty;
/// Register the callback to be called when the object changes.
///
/// If other callbacks have already been registered, they must be removed
/// (with [removeCallback]) before the callback is next called.
void addCallback(ValueGetter<T> callback) => _callbacks.add(callback);
/// Remove a previously registered callback.
///
/// If the given callback is not registered, the call is ignored.
void removeCallback(ValueGetter<T> callback) => _callbacks.remove(callback);
/// Calls the (single) registered callback and returns its result.
///
/// If no callback is registered, or if the callback throws, returns
/// `defaultValue`.
///
/// Call this method whenever the callback is to be invoked. If there is more
/// than one callback registered, this method will throw a [StateError].
///
/// Exceptions thrown by callbacks will be caught and reported using
/// [FlutterError.reportError].
@protected
T invokeCallback(T defaultValue) {
if (_callbacks.isEmpty)
return defaultValue;
try {
return _callbacks.single();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widget library',
context: ErrorDescription('while invoking the callback for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<_CallbackHookProvider<T>>(
'The $runtimeType that invoked the callback was:',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
return defaultValue;
}
}
}
/// Report to a [Router] when the user taps the back button on platforms that
/// support back buttons (such as Android).
///
/// When [Router] widgets are nested, consider using a
/// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher],
/// so that the back button requests get dispatched to the appropriate [Router].
/// To make this work properly, it's important that whenever a [Router] thinks
/// it should get the back button messages (e.g. after the user taps inside it),
/// it calls [takePriority] on its [BackButtonDispatcher] (or
/// [ChildBackButtonDispatcher]) instance.
///
/// The class takes a single callback, which must return a [Future<bool>]. The
/// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely,
/// the callback should return a future that completes to true if it can handle
/// the pop request, and a future that completes to false otherwise.
abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
LinkedHashSet<ChildBackButtonDispatcher>? _children;
@override
bool get hasCallbacks => super.hasCallbacks || (_children != null && _children!.isNotEmpty);
/// Handles a pop route request.
///
/// This method prioritizes the children list in reverse order and calls
/// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them
/// handles the request (by returning a future with true), it exits this
/// method by returning this future. Otherwise, it keeps moving on to the next
/// child until a child handles the request. If none of the children handles
/// the request, this back button dispatcher will then try to handle the request
/// by itself. This back button dispatcher handles the request by notifying the
/// router which in turn calls the [RouterDelegate.popRoute] and returns its
/// result.
///
/// To decide whether this back button dispatcher will handle the pop route
/// request, you can override the [RouterDelegate.popRoute] of the router
/// delegate you pass into the router with this back button dispatcher to
/// return a future of true or false.
@override
Future<bool> invokeCallback(Future<bool> defaultValue) {
if (_children != null && _children!.isNotEmpty) {
final List<ChildBackButtonDispatcher> children = _children!.toList();
int childIndex = children.length - 1;
Future<bool> notifyNextChild(bool result) {
// If the previous child handles the callback, we returns the result.
if (result)
return SynchronousFuture<bool>(result);
// If the previous child did not handle the callback, we ask the next
// child to handle the it.
if (childIndex > 0) {
childIndex -= 1;
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
// If none of the child handles the callback, the parent will then handle it.
return super.invokeCallback(defaultValue);
}
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
return super.invokeCallback(defaultValue);
}
/// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this
/// back button dispatcher.
///
/// To participate in handling the pop route request, call the [takePriority]
/// on the [ChildBackButtonDispatcher] created from this method.
///
/// When the pop route request is handled by this back button dispatcher, it
/// propagate the request to its direct descendants that have called the
/// [takePriority] method. If there are multiple candidates, the latest one
/// that called the [takePriority] wins the right to handle the request. If
/// the latest one does not handle the request (by returning a future of
/// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest
/// one will then have the right to handle the request. This dispatcher
/// continues finding the next candidate until there are no more candidates
/// and finally handles the request itself.
ChildBackButtonDispatcher createChildBackButtonDispatcher() {
return ChildBackButtonDispatcher(this);
}
/// Make this [BackButtonDispatcher] take priority among its peers.
///
/// This has no effect when a [BackButtonDispatcher] has no parents and no
/// children. If a [BackButtonDispatcher] does have parents or children,
/// however, it causes this object to be the one to dispatch the notification
/// when the parent would normally notify its callback.
///
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to take priority.
void takePriority() {
if (_children != null)
_children!.clear();
}
/// Mark the given child as taking priority over this object and the other
/// children.
///
/// This causes [invokeCallback] to defer to the given child instead of
/// calling this object's callback.
///
/// Children are stored in a list, so that if the current child is removed
/// using [forget], a previous child will return to take its place. When
/// [takePriority] is called, the list is cleared.
///
/// Calling this again without first calling [forget] moves the child back to
/// the head of the list.
///
// (Actually it moves it to the end of the list and we treat the end of the
// list to be the priority end, but that's an implementation detail.)
//
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to defer to a child.
void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
_children ??= <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
_children!.remove(child); // child may or may not be in the set already
_children!.add(child);
}
/// Causes the given child to be removed from the list of children to which
/// this object might defer, as if [deferTo] had never been called for that
/// child.
///
/// This should only be called once per child, even if [deferTo] was called
/// multiple times for that child.
///
/// If no children are left in the list, this object will stop deferring to
/// its children. (This is not the same as calling [takePriority], since, if
/// this object itself is a [ChildBackButtonDispatcher], [takePriority] would
/// additionally attempt to claim priority from its parent, whereas removing
/// the last child does not.)
void forget(ChildBackButtonDispatcher child) {
assert(_children != null);
assert(_children!.contains(child));
_children!.remove(child);
}
}
/// The default implementation of back button dispatcher for the root router.
///
/// This dispatcher listens to platform pop route notifications. When the
/// platform wants to pop the current route, this dispatcher calls the
/// [BackButtonDispatcher.invokeCallback] method to handle the request.
class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingObserver {
/// Create a root back button dispatcher.
RootBackButtonDispatcher();
@override
void addCallback(ValueGetter<Future<bool>> callback) {
if (!hasCallbacks)
WidgetsBinding.instance!.addObserver(this);
super.addCallback(callback);
}
@override
void removeCallback(ValueGetter<Future<bool>> callback) {
super.removeCallback(callback);
if (!hasCallbacks)
WidgetsBinding.instance!.removeObserver(this);
}
@override
Future<bool> didPopRoute() => invokeCallback(Future<bool>.value(false));
}
/// A variant of [BackButtonDispatcher] which listens to notifications from a
/// parent back button dispatcher, and can take priority from its parent for the
/// handling of such notifications.
///
/// Useful when [Router]s are being nested within each other.
///
/// Use [Router.of] to obtain a reference to the nearest ancestor [Router], from
/// which the [Router.backButtonDispatcher] can be found, and then used as the
/// [parent] of the [ChildBackButtonDispatcher].
class ChildBackButtonDispatcher extends BackButtonDispatcher {
/// Creates a back button dispatcher that acts as the child of another.
///
/// The [parent] must not be null.
ChildBackButtonDispatcher(this.parent) : assert(parent != null);
/// The back button dispatcher that this object will attempt to take priority
/// over when [takePriority] is called.
///
/// The parent must have a listener registered before this child object can
/// have its [takePriority] or [deferTo] methods used.
final BackButtonDispatcher parent;
/// The parent of this child back button dispatcher decide to let this
/// child to handle the invoke the callback request in
/// [BackButtonDispatcher.invokeCallback].
///
/// Return a boolean future with true if this child will handle the request;
/// otherwise, return a boolean future with false.
@protected
Future<bool> notifiedByParent(Future<bool> defaultValue) {
return invokeCallback(defaultValue);
}
@override
void takePriority() {
parent.deferTo(this);
super.takePriority();
}
@override
void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
super.deferTo(child);
}
@override
void removeCallback(ValueGetter<Future<bool>> callback) {
super.removeCallback(callback);
if (!hasCallbacks)
parent.forget(this);
}
}
/// A delegate that is used by the [Router] widget to parse a route information
/// into a configuration of type T.
///
/// This delegate is used when the [Router] widget is first built with initial
/// route information from [Router.routeInformationProvider] and any subsequent
/// new route notifications from it. The [Router] widget calls the [parseRouteInformation]
/// with the route information from [Router.routeInformationProvider].
abstract class RouteInformationParser<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RouteInformationParser();
/// Converts the given route information into parsed data to pass to a
/// [RouterDelegate].
///
/// The method should return a future which completes when the parsing is
/// complete. The parsing may be asynchronous if, e.g., the parser needs to
/// communicate with the OEM thread to obtain additional data about the route.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to pass the data to the [RouterDelegate].
Future<T> parseRouteInformation(RouteInformation routeInformation);
/// Restore the route information from the given configuration.
///
/// This is not required if you do not opt for the route information reporting
/// , which is used for updating browser history for the web application. If
/// you decides to opt in, you must also overrides this method to return a
/// route information.
///
/// In practice, the [parseRouteInformation] method must produce an equivalent
/// configuration when passed this method's return value
RouteInformation? restoreRouteInformation(T configuration) => null;
}
/// A delegate that is used by the [Router] widget to build and configure a
/// navigating widget.
///
/// This delegate is the core piece of the [Router] widget. It responds to
/// push route and pop route intent from the engine and notifies the [Router]
/// to rebuild. It also act as a builder for the [Router] widget and builds a
/// navigating widget, typically a [Navigator], when the [Router] widget
/// builds.
///
/// When engine pushes a new route, the route information is parsed by the
/// [RouteInformationParser] to produce a configuration of type T. The router
/// delegate receives the configuration through [setInitialRoutePath] or
/// [setNewRoutePath] to configure itself and builds the latest navigating
/// widget upon asked.
///
/// When implementing subclass, consider defining a listenable app state to be
/// used for building the navigating widget. The router delegate should update
/// the app state accordingly and notify the listener know the app state has
/// changed when it receive route related engine intents (e.g.
/// [setNewRoutePath], [setInitialRoutePath], or [popRoute]).
///
/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
///
/// See also:
///
/// * [RouteInformationParser], which is responsible for parsing the route
/// information to a configuration before passing in to router delegate.
/// * [Router], which is the widget that wires all the delegates together to
/// provide a fully functional routing solution.
abstract class RouterDelegate<T> extends Listenable {
/// Called by the [Router] at startup with the structure that the
/// [RouteInformationParser] obtained from parsing the initial route.
///
/// This should configure the [RouterDelegate] so that when [build] is
/// invoked, it will create a widget tree that matches the initial route.
///
/// By default, this method forwards the [configuration] to [setNewRoutePath].
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<void> setInitialRoutePath(T configuration) {
return setNewRoutePath(configuration);
}
/// Called by the [Router] when the [Router.routeInformationProvider] reports that a
/// new route has been pushed to the application by the operating system.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<void> setNewRoutePath(T configuration);
/// Called by the [Router] when the [Router.backButtonDispatcher] reports that
/// the operating system is requesting that the current route be popped.
///
/// The method should return a boolean [Future] to indicate whether this
/// delegate handles the request. Returning false will cause the entire app
/// to be popped.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<bool> popRoute();
/// Called by the [Router] when it detects a route information may have
/// changed as a result of rebuild.
///
/// If this getter returns non-null, the [Router] will start to report new
/// route information back to the engine. In web applications, the new
/// route information is used for populating browser history in order to
/// support the forward and the backward buttons.
///
/// When overriding this method, the configuration returned by this getter
/// must be able to construct the current app state and build the widget
/// with the same configuration in the [build] method if it is passed back
/// to the the [setNewRoutePath]. Otherwise, the browser backward and forward
/// buttons will not work properly.
///
/// By default, this getter returns null, which prevents the [Router] from
/// reporting the route information. To opt in, a subclass can override this
/// getter to return the current configuration.
///
/// At most one [Router] can opt in to route information reporting. Typically,
/// only the top-most [Router] created by [WidgetsApp.router] should opt for
/// route information reporting.
T? get currentConfiguration => null;
/// Called by the [Router] to obtain the widget tree that represents the
/// current state.
///
/// This is called whenever the [setInitialRoutePath] method's future
/// completes, the [setNewRoutePath] method's future completes with the value
/// true, the [popRoute] method's future completes with the value true, or
/// this object notifies its clients (see the [Listenable] interface, which
/// this interface includes). In addition, it may be called at other times. It
/// is important, therefore, that the methods above do not update the state
/// that the [build] method uses before they complete their respective
/// futures.
///
/// Typically this method returns a suitably-configured [Navigator]. If you do
/// plan to create a navigator, consider using the
/// [PopNavigatorRouterDelegateMixin].
///
/// This method must not return null.
///
/// The `context` is the [Router]'s build context.
Widget build(BuildContext context);
}
/// A route information provider that provides route information for the
/// [Router] widget
///
/// This provider is responsible for handing the route information through [value]
/// getter and notifies listeners, typically the [Router] widget, when a new
/// route information is available.
///
/// When the router opts for the route information reporting (by overrides the
/// [RouterDelegate.currentConfiguration] to return non-null), overrides the
/// [routerReportsNewRouteInformation] method to process the route information.
///
/// See also:
///
/// * [PlatformRouteInformationProvider], which wires up the itself with the
/// [WidgetsBindingObserver.didPushRoute] to propagate platform push route
/// intent to the [Router] widget, as well as reports new route information
/// from the [Router] back to the engine by overriding the
/// [routerReportsNewRouteInformation].
abstract class RouteInformationProvider extends ValueListenable<RouteInformation?> {
/// A callback called when the [Router] widget detects any navigation event
/// due to state changes.
///
/// The subclasses can override this method to update theirs values or trigger
/// other side effects. For example, the [PlatformRouteInformationProvider]
/// overrides this method to report the route information back to the engine.
///
/// The [routeInformation] is the new route information after the navigation
/// event.
void routerReportsNewRouteInformation(RouteInformation routeInformation) {}
}
/// The route information provider that propagates the platform route information changes.
///
/// This provider also reports the new route information from the [Router] widget
/// back to engine using message channel method, the
/// [SystemNavigator.routeInformationUpdated].
class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier {
/// Create a platform route information provider.
///
/// Use the [initialRouteInformation] to set the default route information for this
/// provider.
PlatformRouteInformationProvider({
RouteInformation? initialRouteInformation
}) : _value = initialRouteInformation;
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation) {
SystemNavigator.routeInformationUpdated(
location: routeInformation.location!,
state: routeInformation.state,
);
_value = routeInformation;
}
@override
RouteInformation? get value => _value;
RouteInformation? _value;
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation)
return;
_value = routeInformation;
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
if (!hasListeners)
WidgetsBinding.instance!.addObserver(this);
super.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners)
WidgetsBinding.instance!.removeObserver(this);
}
@override
void dispose() {
// In practice, this will rarely be called. We assume that the listeners
// will be added and removed in a coherent fashion such that when the object
// is no longer being used, there's no listener, and so it will get garbage
// collected.
if (hasListeners)
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
assert(hasListeners);
_platformReportsNewRouteInformation(routeInformation);
return true;
}
@override
Future<bool> didPushRoute(String route) async {
assert(hasListeners);
_platformReportsNewRouteInformation(RouteInformation(location: route));
return true;
}
}
/// A mixin that wires [RouterDelegate.popRoute] to the [Navigator] it builds.
///
/// This mixin calls [Navigator.maybePop] when it receives an Android back
/// button intent through the [RouterDelegate.popRoute]. Using this mixin
/// guarantees that the back button still respects pageless routes in the
/// navigator.
///
/// Only use this mixin if you plan to build a navigator in the
/// [RouterDelegate.build].
mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
/// The key used for retrieving the current navigator.
///
/// When using this mixin, be sure to use this key to create the navigator.
GlobalKey<NavigatorState>? get navigatorKey;
@override
Future<bool> popRoute() {
final NavigatorState? navigator = navigatorKey?.currentState;
if (navigator == null)
return SynchronousFuture<bool>(false);
return navigator.maybePop();
}
}