blob: 27428948a96b6d97fde27a6434da5ed86203bb18 [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';
import 'restoration.dart';
import 'restoration_properties.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 happens in web application where the [Router] reports route
/// changes back to the web engine.
///
/// The current [RouteInformation] of an application is also used for state
/// restoration purposes. Before an application is killed, the [Router] converts
/// its current configurations into a [RouteInformation] object utilizing the
/// [RouteInformationProvider]. The [RouteInformation] object is then serialized
/// out and persisted. During state restoration, the object is deserialized and
/// passed back to the [RouteInformationProvider], which turns it into a
/// configuration for the [Router] again to restore its state from.
class RouteInformation {
/// Creates a route information object.
///
/// The arguments may be null.
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].
///
/// On the web, this information is stored in the browser history when the
/// [Router] reports this route information back to the web engine
/// through the [PlatformRouteInformationProvider]. The information
/// is then passed back, along with the [location], when the user
/// clicks the back or forward buttons.
///
/// This information is also serialized and persisted alongside the
/// [location] for state restoration purposes. During state restoration,
/// the information is made available again to the [Router] so it can restore
/// its configuration to the previous state.
///
/// The state must be serializable.
final Object? state;
}
/// A convenient bundle to configure a [Router] widget.
///
/// To configure a [Router] widget, one needs to provide several delegates,
/// [RouteInformationProvider], [RouteInformationParser], [RouterDelegate],
/// and [BackButtonDispatcher]. This abstract class provides way to bundle these
/// delegates into a single object to configure a [Router].
///
/// The [routerDelegate] must not be null. The [backButtonDispatcher],
/// [routeInformationProvider], and [routeInformationProvider] are optional.
///
/// The [routeInformationProvider] and [routeInformationParser] must
/// both be provided or not provided.
class RouterConfig<T> {
/// Creates a [RouterConfig].
///
/// The [routerDelegate] must not be null. The [backButtonDispatcher],
/// [routeInformationProvider], and [routeInformationParser] are optional.
///
/// The [routeInformationProvider] and [routeInformationParser] must
/// both be provided or not provided.
const RouterConfig({
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
}) : assert((routeInformationProvider == null) == (routeInformationParser == null));
/// The [RouteInformationProvider] that is used to configure the [Router].
final RouteInformationProvider? routeInformationProvider;
/// The [RouteInformationParser] that is used to configure the [Router].
final RouteInformationParser<T>? routeInformationParser;
/// The [RouterDelegate] that is used to configure the [Router].
final RouterDelegate<T> routerDelegate;
/// The [BackButtonDispatcher] that is used to configure the [Router].
final BackButtonDispatcher? backButtonDispatcher;
}
/// 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].
///
/// Each part of this process 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] is used as the
/// initial route when the [Router] is first created. 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 (e.g. iOS and desktop platforms); on those platforms this
/// notification is never sent. Typically, the [backButtonDispatcher] for the
/// root router is an instance of [RootBackButtonDispatcher], which uses a
/// [WidgetsBindingObserver] to listen to the `popRoute` notifications from
/// [SystemChannels.navigation]. Nested [Router]s typically use a
/// [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 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. This is common
/// for desktop applications, where subsidiary "screens" are represented using
/// different windows rather than changing the active interface.
///
/// 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, keeping the URL in the browser's location bar up to
/// date with the application state ensures that the browser constructs its
/// history entry correctly, allowing its back and forward buttons to function
/// as the user expects.
///
/// If an app state change leads to the [Router] rebuilding, the [Router] will
/// retrieve the new route information from the [routerDelegate]'s
/// [RouterDelegate.currentConfiguration] method and the
/// [routeInformationParser]'s [RouteInformationParser.restoreRouteInformation]
/// method.
///
/// If the location in the new route information is different from the
/// current location, this is considered to be a navigation event, the
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] method
/// calls [SystemNavigator.routeInformationUpdated] with `replace = false` to
/// notify the engine, and through that the browser, to create a history entry
/// with the new url. Otherwise,
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] calls
/// [SystemNavigator.routeInformationUpdated] with `replace = true` to update
/// the current history entry with the latest [RouteInformation].
///
/// One can force the [Router] to report new route information as navigation
/// event to the [routeInformationProvider] (and thus the browser) even if the
/// [RouteInformation.location] has not changed by calling the [Router.navigate]
/// method with a callback that performs the state change. This causes [Router]
/// to call the [RouteInformationProvider.routerReportsNewRouteInformation] with
/// [RouteInformationReportingType.navigate], and thus causes
/// [PlatformRouteInformationProvider] to push a new history entry regardlessly.
/// This allows one to support the browser's back and forward buttons without
/// changing the URL. For example, the scroll position of a scroll view may be
/// saved in the [RouteInformation.state]. Using [Router.navigate] to update the
/// scroll position causes the browser to create a new history entry with the
/// [RouteInformation.state] that stores this new scroll position. When the user
/// clicks the back button, the app will go back to the previous scroll position
/// without changing the URL in the location bar.
///
/// One can also force the [Router] to ignore a navigation event by making
/// those changes during a callback passed to [Router.neglect]. The [Router]
/// calls the [RouteInformationProvider.routerReportsNewRouteInformation] with
/// [RouteInformationReportingType.neglect], and thus causes
/// [PlatformRouteInformationProvider] to replace the current history entry
/// regardlessly even if it detects location change.
///
/// To opt out of URL updates entirely, pass null for [routeInformationProvider]
/// and [routeInformationParser]. This is not recommended in general, but may be
/// appropriate in the following cases:
///
/// * The application does not target the web platform.
///
/// * There are multiple router widgets in the application. Only one [Router]
/// widget should update the URL (typically the top-most one created by the
/// [WidgetsApp.router], [MaterialApp.router], or [CupertinoApp.router]).
///
/// * The application does not need to implement in-app navigation using the
/// browser's back and forward buttons.
///
/// In other cases, it is strongly recommended to implement the
/// [RouterDelegate.currentConfiguration] and
/// [RouteInformationParser.restoreRouteInformation] APIs to provide an optimal
/// user experience when running on the web platform.
///
/// ## State Restoration
///
/// The [Router] will restore the current configuration of the [routerDelegate]
/// during state restoration if it is configured with a [restorationScopeId] and
/// state restoration is enabled for the subtree. For that, the value of
/// [RouterDelegate.currentConfiguration] is serialized and persisted before the
/// app is killed by the operating system. After the app is restarted, the value
/// is deserialized and passed back to the [RouterDelegate] via a call to
/// [RouterDelegate.setRestoredRoutePath] (which by default just calls
/// [RouterDelegate.setNewRoutePath]). It is the responsibility of the
/// [RouterDelegate] to use the configuration information provided to restore
/// its internal state.
///
/// To serialize [RouterDelegate.currentConfiguration] and to deserialize it
/// again, the [Router] calls [RouteInformationParser.restoreRouteInformation]
/// and [RouteInformationParser.parseRouteInformation], respectively. Therefore,
/// if a [restorationScopeId] is provided, a [routeInformationParser] must be
/// configured as well.
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 based on the app state.
///
/// The [routeInformationProvider] and [routeInformationParser] must
/// both be provided or not provided.
///
/// The [routerDelegate] must not be null.
const Router({
super.key,
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
this.restorationScopeId,
}) : assert(
routeInformationProvider == null || routeInformationParser != null,
'A routeInformationParser must be provided when a routeInformationProvider is specified.',
),
assert(routerDelegate != null);
/// Creates a router with a [RouterConfig].
///
/// The [RouterConfig.routeInformationProvider] and
/// [RouterConfig.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 based on the app state.
///
/// If the [RouterConfig.routeInformationProvider] is not null, then
/// [RouterConfig.routeInformationParser] must also not be
/// null.
///
/// The [RouterConfig.routerDelegate] must not be null.
factory Router.withConfig({
Key? key,
required RouterConfig<T> config,
String? restorationScopeId,
}) {
return Router<T>(
key: key,
routeInformationProvider: config.routeInformationProvider,
routeInformationParser: config.routeInformationParser,
routerDelegate: config.routerDelegate,
backButtonDispatcher: config.backButtonDispatcher,
restorationScopeId: restorationScopeId,
);
}
/// 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] must 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 [RouterDelegate.popRoute] to handle the back
/// button.
///
/// 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;
/// Restoration ID to save and restore the state of the [Router].
///
/// If non-null, the [Router] will persist the [RouterDelegate]'s current
/// configuration (i.e. [RouterDelegate.currentConfiguration]). During state
/// restoration, the [Router] informs the [RouterDelegate] of the previous
/// configuration by calling [RouterDelegate.setRestoredRoutePath] (which by
/// default just calls [RouterDelegate.setNewRoutePath]). It is the
/// responsibility of the [RouterDelegate] to restore its internal state based
/// on the provided configuration.
///
/// The router uses the [RouteInformationParser] to serialize and deserialize
/// [RouterDelegate.currentConfiguration]. Therefore, a
/// [routeInformationParser] must be provided when [restorationScopeId] is
/// non-null.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationScopeId;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// This method provides access to the delegates in the [Router]. For example,
/// this can be used to access the [backButtonDispatcher] of the parent router
/// when creating a [ChildBackButtonDispatcher] for a nested [Router].
///
/// If no [Router] ancestor exists for the given context, this will assert in
/// debug mode, and throw an exception in release mode.
///
/// See also:
///
/// * [maybeOf], which is a similar function, but it will return null instead
/// of throwing an exception if no [Router] ancestor exists.
static Router<T> of<T extends Object?>(BuildContext context) {
final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
assert(() {
if (scope == null) {
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 as Router<T>;
}
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// This method provides access to the delegates in the [Router]. For example,
/// this can be used to access the [backButtonDispatcher] of the parent router
/// when creating a [ChildBackButtonDispatcher] for a nested [Router].
///
/// If no `Router` ancestor exists for the given context, this will return
/// null.
///
/// See also:
///
/// * [of], a similar method that returns a non-nullable value, and will
/// throw if no [Router] ancestor exists.
static Router<T>? maybeOf<T extends Object?>(BuildContext context) {
final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
return scope?.routerState.widget as Router<T>?;
}
/// Forces the [Router] to run the [callback] and create a new history
/// entry in the browser.
///
/// 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 create a new history entry
/// even if location does change.
static void navigate(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()!
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(RouteInformationReportingType.navigate, callback);
}
/// Forces the [Router] to run the [callback] without creating a new history
/// entry in the browser.
///
/// 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.
///
/// Creating a new route history entry makes users feel they have visited a
/// new page, and the browser back button brings them back to previous history
/// entry. Use this method if you don't want the [Router] to create a new
/// route information even if it detects changes as a result of running the
/// [callback].
///
/// Using this method will still update the URL and state in current 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 create a new history entry
/// 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(RouteInformationReportingType.neglect, callback);
}
@override
State<Router<T>> createState() => _RouterState<T>();
}
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
typedef _RouteSetter<T> = Future<void> Function(T);
/// The [Router]'s intention when it reports a new [RouteInformation] to the
/// [RouteInformationProvider].
///
/// See also:
///
/// * [RouteInformationProvider.routerReportsNewRouteInformation]: which is
/// called by the router when it has a new route information to report.
enum RouteInformationReportingType {
/// Router does not have a specific intention.
///
/// The router generates a new route information every time it detects route
/// information may have change due to a rebuild. This is the default type if
/// neither [Router.neglect] nor [Router.navigate] was used during the
/// rebuild.
none,
/// The accompanying [RouteInformation] were generated during a
/// [Router.neglect] call.
neglect,
/// The accompanying [RouteInformation] were generated during a
/// [Router.navigate] call.
navigate,
}
class _RouterState<T> extends State<Router<T>> with RestorationMixin {
Object? _currentRouterTransaction;
RouteInformationReportingType? _currentIntentionToReport;
final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
late bool _routeParsePending;
@override
String? get restorationId => widget.restorationScopeId;
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_routeInformation, 'route');
if (_routeInformation.value != null) {
assert(widget.routeInformationParser != null);
_processRouteInformation(_routeInformation.value!, () => widget.routerDelegate.setRestoredRoutePath);
} else if (widget.routeInformationProvider != null) {
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setInitialRoutePath);
}
}
bool _routeInformationReportingTaskScheduled = false;
void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null) {
return;
}
assert(_currentIntentionToReport != null);
_routeInformationReportingTaskScheduled = true;
SchedulerBinding.instance.addPostFrameCallback(_reportRouteInformation);
}
void _reportRouteInformation(Duration timestamp) {
assert(_routeInformationReportingTaskScheduled);
_routeInformationReportingTaskScheduled = false;
if (_routeInformation.value != null) {
final RouteInformation currentRouteInformation = _routeInformation.value!;
assert(_currentIntentionToReport != null);
widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation, type: _currentIntentionToReport!);
}
_currentIntentionToReport = RouteInformationReportingType.none;
}
RouteInformation? _retrieveNewRouteInformation() {
final T? configuration = widget.routerDelegate.currentConfiguration;
if (configuration == null) {
return null;
}
return widget.routeInformationParser?.restoreRouteInformation(configuration);
}
void _setStateWithExplicitReportStatus(
RouteInformationReportingType status,
VoidCallback fn,
) {
assert(status != null);
assert(status.index >= RouteInformationReportingType.neglect.index);
assert(() {
if (_currentIntentionToReport != null &&
_currentIntentionToReport != RouteInformationReportingType.none &&
_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() {
_routeInformation.value = _retrieveNewRouteInformation();
_currentIntentionToReport ??= RouteInformationReportingType.none;
_scheduleRouteInformationReportingTask();
}
@override
void didChangeDependencies() {
_routeParsePending = true;
super.didChangeDependencies();
// The super.didChangeDependencies may have parsed the route information.
// This can happen if the didChangeDependencies is triggered by state
// restoration or first build.
if (widget.routeInformationProvider != null && _routeParsePending) {
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
_routeParsePending = false;
_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) {
_currentRouterTransaction = 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);
_currentRouterTransaction = null;
super.dispose();
}
void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
assert(_routeParsePending);
_routeParsePending = false;
_currentRouterTransaction = Object();
widget.routeInformationParser!
.parseRouteInformationWithDependencies(information, context)
.then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
}
_RouteSetter<T> _processParsedRouteInformation(Object? transaction, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
return (T data) async {
if (_currentRouterTransaction != transaction) {
return;
}
await delegateRouteSetter()(data);
if (_currentRouterTransaction == transaction) {
_rebuild();
}
};
}
void _handleRouteInformationProviderNotification() {
assert(widget.routeInformationProvider!.value != null);
_routeParsePending = true;
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
Future<bool> _handleBackButtonDispatcherNotification() {
_currentRouterTransaction = Object();
return widget.routerDelegate
.popRoute()
.then<bool>(_handleRoutePopped(_currentRouterTransaction));
}
_AsyncPassthrough<bool> _handleRoutePopped(Object? transaction) {
return (bool data) {
if (transaction != _currentRouterTransaction) {
// A rebuilt was trigger from a different source. Returns true to
// prevent bubbling.
return SynchronousFuture<bool>(true);
}
_rebuild();
return SynchronousFuture<bool>(data);
};
}
Future<void> _rebuild([void value]) {
setState(() {/* routerDelegate is ready to rebuild */});
_maybeNeedToReportRouteInformation();
return SynchronousFuture<void>(value);
}
void _handleRouterDelegateNotification() {
setState(() {/* routerDelegate wants to rebuild */});
_maybeNeedToReportRouteInformation();
}
@override
Widget build(BuildContext context) {
return UnmanagedRestorationScope(
bucket: bucket,
child: _RouterScope(
routeInformationProvider: widget.routeInformationProvider,
backButtonDispatcher: widget.backButtonDispatcher,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
routerState: this,
child: Builder(
// Use a Builder so that the build method below will have a
// BuildContext that contains the _RouterScope. This also prevents
// dependencies look ups in routerDelegate from rebuilding Router
// widget that may result in re-parsing the route information.
builder: widget.routerDelegate.build,
),
),
);
}
}
class _RouterScope extends InheritedWidget {
const _RouterScope({
required this.routeInformationProvider,
required this.backButtonDispatcher,
required this.routeInformationParser,
required this.routerDelegate,
required this.routerState,
required super.child,
}) : assert(routeInformationProvider == null || routeInformationParser != null),
assert(routerDelegate != null),
assert(routerState != null);
final ValueListenable<RouteInformation?>? routeInformationProvider;
final BackButtonDispatcher? backButtonDispatcher;
final RouteInformationParser<Object?>? routeInformationParser;
final RouterDelegate<Object?> routerDelegate;
final _RouterState<Object?> 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 signaling.
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
@pragma('vm:notify-debugger-on-exception')
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: () => <DiagnosticsNode>[
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>> {
late final LinkedHashSet<ChildBackButtonDispatcher> _children =
<ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
@override
bool get hasCallbacks => super.hasCallbacks || (_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.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 return 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() => _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.
///
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to defer to a child.
void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
_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) => _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);
parent.deferTo(this);
super.deferTo(child);
}
@override
void removeCallback(ValueGetter<Future<bool>> callback) {
super.removeCallback(callback);
if (!hasCallbacks) {
parent.forget(this);
}
}
}
/// A convenience widget that registers a callback for when the back button is pressed.
///
/// In order to use this widget, there must be an ancestor [Router] widget in the tree
/// that has a [RootBackButtonDispatcher]. e.g. The [Router] widget created by the
/// [MaterialApp.router] has a built-in [RootBackButtonDispatcher] by default.
///
/// It only applies to platforms that accept back button clicks, such as Android.
///
/// It can be useful for scenarios, in which you create a different state in your
/// screen but don't want to use a new page for that.
class BackButtonListener extends StatefulWidget {
/// Creates a BackButtonListener widget .
///
/// The [child] and [onBackButtonPressed] arguments must not be null.
const BackButtonListener({
super.key,
required this.child,
required this.onBackButtonPressed,
});
/// The widget below this widget in the tree.
final Widget child;
/// The callback function that will be called when the back button is pressed.
///
/// It must return a boolean future with true if this child will handle the request;
/// otherwise, return a boolean future with false.
final ValueGetter<Future<bool>> onBackButtonPressed;
@override
State<BackButtonListener> createState() => _BackButtonListenerState();
}
class _BackButtonListenerState extends State<BackButtonListener> {
BackButtonDispatcher? dispatcher;
@override
void didChangeDependencies() {
dispatcher?.removeCallback(widget.onBackButtonPressed);
final BackButtonDispatcher? rootBackDispatcher = Router.of(context).backButtonDispatcher;
assert(rootBackDispatcher != null, 'The parent router must have a backButtonDispatcher to use this widget');
dispatcher = rootBackDispatcher!.createChildBackButtonDispatcher()
..addCallback(widget.onBackButtonPressed)
..takePriority();
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant BackButtonListener oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.onBackButtonPressed != widget.onBackButtonPressed) {
dispatcher?.removeCallback(oldWidget.onBackButtonPressed);
dispatcher?.addCallback(widget.onBackButtonPressed);
dispatcher?.takePriority();
}
}
@override
void dispose() {
dispatcher?.removeCallback(widget.onBackButtonPressed);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}
/// 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].
///
/// One of the [parseRouteInformation] or
/// [parseRouteInformationWithDependencies] must be implemented, otherwise a
/// runtime error will be thrown.
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();
/// {@template flutter.widgets.RouteInformationParser.parseRouteInformation}
/// 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].
/// {@endtemplate}
///
/// One can implement [parseRouteInformationWithDependencies] instead if
/// the parsing depends on other dependencies from the [BuildContext].
Future<T> parseRouteInformation(RouteInformation routeInformation) {
throw UnimplementedError(
'One of the parseRouteInformation or '
'parseRouteInformationWithDependencies must be implemented'
);
}
/// {@macro flutter.widgets.RouteInformationParser.parseRouteInformation}
///
/// The input [BuildContext] can be used for looking up [InheritedWidget]s
/// If one uses [BuildContext.dependOnInheritedWidgetOfExactType], a
/// dependency will be created. The [Router] will re-parse the
/// [RouteInformation] from its [RouteInformationProvider] if the dependency
/// notifies its listeners.
///
/// One can also use [BuildContext.getElementForInheritedWidgetOfExactType] to
/// look up [InheritedWidget]s without creating dependencies.
Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
return parseRouteInformation(routeInformation);
}
/// Restore the route information from the given configuration.
///
/// This may return null, in which case the browser history will not be
/// updated and state restoration is disabled. See [Router]'s documentation
/// for details.
///
/// 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 intents from the engine and notifies the [Router]
/// to rebuild. It also acts as a builder for the [Router] widget and builds a
/// navigating widget, typically a [Navigator], when the [Router] widget
/// builds.
///
/// When the 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 when asked ([build]).
///
/// When implementing subclasses, consider defining a [Listenable] app state object to be
/// used for building the navigating widget. The router delegate would update
/// the app state accordingly and notify its own listeners when the app state has
/// changed and when it receive route related engine intents (e.g.
/// [setNewRoutePath], [setInitialRoutePath], or [popRoute]).
///
/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
///
/// ## State Restoration
///
/// If the [Router] owning this delegate is configured for state restoration, it
/// will persist and restore the configuration of this [RouterDelegate] using
/// the following mechanism: Before the app is killed by the operating system,
/// the value of [currentConfiguration] is serialized out and persisted. After
/// the app has restarted, the value is deserialized and passed back to the
/// [RouterDelegate] via a call to [setRestoredRoutePath] (which by default just
/// calls [setNewRoutePath]). It is the responsibility of the [RouterDelegate]
/// to use the configuration information provided to restore its internal state.
///
/// 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.
///
/// See also:
///
/// * [setRestoredRoutePath], which is called instead of this method during
/// state restoration.
Future<void> setInitialRoutePath(T configuration) {
return setNewRoutePath(configuration);
}
/// Called by the [Router] during state restoration.
///
/// When the [Router] is configured for state restoration, it will persist
/// the value of [currentConfiguration] during state serialization. During
/// state restoration, the [Router] calls this method (instead of
/// [setInitialRoutePath]) to pass the previous configuration back to the
/// delegate. It is the responsibility of the delegate to restore its internal
/// state based on the provided configuration.
///
/// By default, this method forwards the `configuration` to [setNewRoutePath].
Future<void> setRestoredRoutePath(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 [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.
///
/// ## State Restoration
///
/// This getter is also used by the [Router] to implement state restoration.
/// During state serialization, the [Router] will persist the current
/// configuration and during state restoration pass it back to the delegate
/// by calling [setRestoredRoutePath].
T? get currentConfiguration => null;
/// Called by the [Router] to obtain the widget tree that represents the
/// current state.
///
/// This is called whenever the [Future]s returned by [setInitialRoutePath],
/// [setNewRoutePath], or [setRestoredRoutePath] complete as well as when this
/// 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]. If state restoration is enabled for the
/// [Router] using this delegate, consider providing a non-null
/// [Navigator.restorationScopeId] to the [Navigator] returned by this method.
///
/// 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 route information reporting (by overriding the
/// [RouterDelegate.currentConfiguration] to return non-null), override 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 reports new route information
///
/// 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 generated by the
/// Router rebuild, and it can be the same or different from the
/// [value].
///
/// The `type` denotes the [Router]'s intention when it reports this
/// `routeInformation`. It is useful when deciding how to update the internal
/// state of [RouteInformationProvider] subclass with the `routeInformation`.
/// For example, [PlatformRouteInformationProvider] uses this property to
/// decide whether to push or replace the browser history entry with the new
/// `routeInformation`.
///
/// For more information on how [Router] determines a navigation event, see
/// the "URL updates for web applications" section in the [Router]
/// documentation.
void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) {}
}
/// 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].
///
/// Each time [SystemNavigator.routeInformationUpdated] is called, the
/// [SystemNavigator.selectMultiEntryHistory] method is also called. This
/// overrides the initialization behavior of
/// [Navigator.reportsRouteUpdateToEngine].
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({
required RouteInformation initialRouteInformation,
}) : _value = initialRouteInformation;
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) {
final bool replace =
type == RouteInformationReportingType.neglect ||
(type == RouteInformationReportingType.none &&
_valueInEngine.location == routeInformation.location);
SystemNavigator.selectMultiEntryHistory();
SystemNavigator.routeInformationUpdated(
location: routeInformation.location!,
state: routeInformation.state,
replace: replace,
);
_value = routeInformation;
_valueInEngine = routeInformation;
}
@override
RouteInformation get value => _value;
RouteInformation _value;
RouteInformation _valueInEngine = RouteInformation(location: WidgetsBinding.instance.platformDispatcher.defaultRouteName);
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation) {
return;
}
_value = routeInformation;
_valueInEngine = 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();
}
}
class _RestorableRouteInformation extends RestorableValue<RouteInformation?> {
@override
RouteInformation? createDefaultValue() => null;
@override
void didUpdateValue(RouteInformation? oldValue) {
notifyListeners();
}
@override
RouteInformation? fromPrimitives(Object? data) {
if (data == null) {
return null;
}
assert(data is List<Object?> && data.length == 2);
final List<Object?> castedData = data as List<Object?>;
return RouteInformation(location: castedData.first as String?, state: castedData.last);
}
@override
Object? toPrimitives() {
return value == null ? null : <Object?>[value!.location, value!.state];
}
}