1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/cupertino.dart';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'app.dart';
9/// @docImport 'scroll_view.dart';
10library;
11
12import 'dart:async';
13import 'dart:collection';
14
15import 'package:collection/collection.dart';
16import 'package:flutter/foundation.dart';
17import 'package:flutter/scheduler.dart';
18import 'package:flutter/services.dart';
19
20import 'basic.dart';
21import 'binding.dart';
22import 'framework.dart';
23import 'navigator.dart';
24import 'restoration.dart';
25import 'restoration_properties.dart';
26
27/// A piece of routing information.
28///
29/// The route information consists of a location string of the application and
30/// a state object that configures the application in that location.
31///
32/// This information flows two ways, from the [RouteInformationProvider] to the
33/// [Router] or from the [Router] to [RouteInformationProvider].
34///
35/// In the former case, the [RouteInformationProvider] notifies the [Router]
36/// widget when a new [RouteInformation] is available. The [Router] widget takes
37/// these information and navigates accordingly.
38///
39/// The latter case happens in web application where the [Router] reports route
40/// changes back to the web engine.
41///
42/// The current [RouteInformation] of an application is also used for state
43/// restoration purposes. Before an application is killed, the [Router] converts
44/// its current configurations into a [RouteInformation] object utilizing the
45/// [RouteInformationProvider]. The [RouteInformation] object is then serialized
46/// out and persisted. During state restoration, the object is deserialized and
47/// passed back to the [RouteInformationProvider], which turns it into a
48/// configuration for the [Router] again to restore its state from.
49class RouteInformation {
50 /// Creates a route information object.
51 ///
52 /// Either `location` or `uri` must not be null.
53 const RouteInformation({
54 @Deprecated(
55 'Pass Uri.parse(location) to uri parameter instead. '
56 'This feature was deprecated after v3.8.0-3.0.pre.',
57 )
58 String? location,
59 Uri? uri,
60 this.state,
61 }) : _location = location,
62 _uri = uri,
63 assert((location != null) != (uri != null));
64
65 /// The location of the application.
66 ///
67 /// The string is usually in the format of multiple string identifiers with
68 /// slashes in between. ex: `/`, `/path`, `/path/to/the/app`.
69 @Deprecated(
70 'Use uri instead. '
71 'This feature was deprecated after v3.8.0-3.0.pre.',
72 )
73 String get location {
74 return _location ??
75 Uri.decodeComponent(
76 Uri(
77 path: uri.path.isEmpty ? '/' : uri.path,
78 queryParameters: uri.queryParametersAll.isEmpty ? null : uri.queryParametersAll,
79 fragment: uri.fragment.isEmpty ? null : uri.fragment,
80 ).toString(),
81 );
82 }
83
84 final String? _location;
85
86 /// The uri location of the application.
87 ///
88 /// The host and scheme will not be empty if this object is created from a
89 /// deep link request. They represents the website that redirect the deep
90 /// link.
91 ///
92 /// In web platform, the host and scheme are always empty.
93 Uri get uri {
94 if (_uri != null) {
95 return _uri;
96 }
97 return Uri.parse(_location!);
98 }
99
100 final Uri? _uri;
101
102 /// The state of the application in the [uri].
103 ///
104 /// The app can have different states even in the same location. For example,
105 /// the text inside a [TextField] or the scroll position in a [ScrollView].
106 /// These widget states can be stored in the [state].
107 ///
108 /// On the web, this information is stored in the browser history when the
109 /// [Router] reports this route information back to the web engine
110 /// through the [PlatformRouteInformationProvider]. The information
111 /// is then passed back, along with the [uri], when the user
112 /// clicks the back or forward buttons.
113 ///
114 /// This information is also serialized and persisted alongside the
115 /// [uri] for state restoration purposes. During state restoration,
116 /// the information is made available again to the [Router] so it can restore
117 /// its configuration to the previous state.
118 ///
119 /// The state must be serializable.
120 final Object? state;
121}
122
123/// A convenient bundle to configure a [Router] widget.
124///
125/// To configure a [Router] widget, one needs to provide several delegates,
126/// [RouteInformationProvider], [RouteInformationParser], [RouterDelegate],
127/// and [BackButtonDispatcher]. This abstract class provides way to bundle these
128/// delegates into a single object to configure a [Router].
129///
130/// The [backButtonDispatcher], [routeInformationProvider], and
131/// [routeInformationProvider] are optional.
132///
133/// The [routeInformationProvider] and [routeInformationParser] must
134/// both be provided or both not provided.
135class RouterConfig<T> {
136 /// Creates a [RouterConfig].
137 ///
138 /// The [backButtonDispatcher], [routeInformationProvider], and
139 /// [routeInformationParser] are optional.
140 ///
141 /// The [routeInformationProvider] and [routeInformationParser] must both be
142 /// provided or both not provided.
143 const RouterConfig({
144 this.routeInformationProvider,
145 this.routeInformationParser,
146 required this.routerDelegate,
147 this.backButtonDispatcher,
148 }) : assert((routeInformationProvider == null) == (routeInformationParser == null));
149
150 /// The [RouteInformationProvider] that is used to configure the [Router].
151 final RouteInformationProvider? routeInformationProvider;
152
153 /// The [RouteInformationParser] that is used to configure the [Router].
154 final RouteInformationParser<T>? routeInformationParser;
155
156 /// The [RouterDelegate] that is used to configure the [Router].
157 final RouterDelegate<T> routerDelegate;
158
159 /// The [BackButtonDispatcher] that is used to configure the [Router].
160 final BackButtonDispatcher? backButtonDispatcher;
161}
162
163/// The dispatcher for opening and closing pages of an application.
164///
165/// This widget listens for routing information from the operating system (e.g.
166/// an initial route provided on app startup, a new route obtained when an
167/// intent is received, or a notification that the user hit the system back
168/// button), parses route information into data of type `T`, and then converts
169/// that data into [Page] objects that it passes to a [Navigator].
170///
171/// Each part of this process can be overridden and configured as desired.
172///
173/// The [routeInformationProvider] can be overridden to change how the name of
174/// the route is obtained. The [RouteInformationProvider.value] is used as the
175/// initial route when the [Router] is first created. Subsequent notifications
176/// from the [RouteInformationProvider] to its listeners are treated as
177/// notifications that the route information has changed.
178///
179/// The [backButtonDispatcher] can be overridden to change how back button
180/// notifications are received. This must be a [BackButtonDispatcher], which is
181/// an object where callbacks can be registered, and which can be chained so
182/// that back button presses are delegated to subsidiary routers. The callbacks
183/// are invoked to indicate that the user is trying to close the current route
184/// (by pressing the system back button); the [Router] ensures that when this
185/// callback is invoked, the message is passed to the [routerDelegate] and its
186/// result is provided back to the [backButtonDispatcher]. Some platforms don't
187/// have back buttons (e.g. iOS and desktop platforms); on those platforms this
188/// notification is never sent. Typically, the [backButtonDispatcher] for the
189/// root router is an instance of [RootBackButtonDispatcher], which uses a
190/// [WidgetsBindingObserver] to listen to the `popRoute` notifications from
191/// [SystemChannels.navigation]. Nested [Router]s typically use a
192/// [ChildBackButtonDispatcher], which must be provided the
193/// [BackButtonDispatcher] of its ancestor [Router] (available via [Router.of]).
194///
195/// The [routeInformationParser] can be overridden to change how names obtained
196/// from the [routeInformationProvider] are interpreted. It must implement the
197/// [RouteInformationParser] interface, specialized with the same type as the
198/// [Router] itself. This type, `T`, represents the data type that the
199/// [routeInformationParser] will generate.
200///
201/// The [routerDelegate] can be overridden to change how the output of the
202/// [routeInformationParser] is interpreted. It must implement the
203/// [RouterDelegate] interface, also specialized with `T`; it takes as input
204/// the data (of type `T`) from the [routeInformationParser], and is responsible
205/// for providing a navigating widget to insert into the widget tree. The
206/// [RouterDelegate] interface is also [Listenable]; notifications are taken
207/// to mean that the [Router] needs to rebuild.
208///
209/// ## Concerns regarding asynchrony
210///
211/// Some of the APIs (notably those involving [RouteInformationParser] and
212/// [RouterDelegate]) are asynchronous.
213///
214/// When developing objects implementing these APIs, if the work can be done
215/// entirely synchronously, then consider using [SynchronousFuture] for the
216/// future returned from the relevant methods. This will allow the [Router] to
217/// proceed in a completely synchronous way, which removes a number of
218/// complications.
219///
220/// Using asynchronous computation is entirely reasonable, however, and the API
221/// is designed to support it. For example, maybe a set of images need to be
222/// loaded before a route can be shown; waiting for those images to be loaded
223/// before [RouterDelegate.setNewRoutePath] returns is a reasonable approach to
224/// handle this case.
225///
226/// If an asynchronous operation is ongoing when a new one is to be started, the
227/// precise behavior will depend on the exact circumstances, as follows:
228///
229/// If the active operation is a [routeInformationParser] parsing a new route information:
230/// that operation's result, if it ever completes, will be discarded.
231///
232/// If the active operation is a [routerDelegate] handling a pop request:
233/// the previous pop is immediately completed with "false", claiming that the
234/// previous pop was not handled (this may cause the application to close).
235///
236/// If the active operation is a [routerDelegate] handling an initial route
237/// or a pushed route, the result depends on the new operation. If the new
238/// operation is a pop request, then the original operation's result, if it ever
239/// completes, will be discarded. If the new operation is a push request,
240/// however, the [routeInformationParser] will be requested to start the parsing, and
241/// only if that finishes before the original [routerDelegate] request
242/// completes will that original request's result be discarded.
243///
244/// If the identity of the [Router] widget's delegates change while an
245/// asynchronous operation is in progress, to keep matters simple, all active
246/// asynchronous operations will have their results discarded. It is generally
247/// considered unusual for these delegates to change during the lifetime of the
248/// [Router].
249///
250/// If the [Router] itself is disposed while an asynchronous operation is in
251/// progress, all active asynchronous operations will have their results
252/// discarded also.
253///
254/// No explicit signals are provided to the [routeInformationParser] or
255/// [routerDelegate] to indicate when any of the above happens, so it is
256/// strongly recommended that [RouteInformationParser] and [RouterDelegate]
257/// implementations not perform extensive computation.
258///
259/// ## Application architectural design
260///
261/// An application can have zero, one, or many [Router] widgets, depending on
262/// its needs.
263///
264/// An application might have no [Router] widgets if it has only one "screen",
265/// or if the facilities provided by [Navigator] are sufficient. This is common
266/// for desktop applications, where subsidiary "screens" are represented using
267/// different windows rather than changing the active interface.
268///
269/// A particularly elaborate application might have multiple [Router] widgets,
270/// in a tree configuration, with the first handling the entire route parsing
271/// and making the result available for routers in the subtree. The routers in
272/// the subtree do not participate in route information parsing but merely take the
273/// result from the first router to build their sub routes.
274///
275/// Most applications only need a single [Router].
276///
277/// ## URL updates for web applications
278///
279/// In the web platform, keeping the URL in the browser's location bar up to
280/// date with the application state ensures that the browser constructs its
281/// history entry correctly, allowing its back and forward buttons to function
282/// as the user expects.
283///
284/// If an app state change leads to the [Router] rebuilding, the [Router] will
285/// retrieve the new route information from the [routerDelegate]'s
286/// [RouterDelegate.currentConfiguration] method and the
287/// [routeInformationParser]'s [RouteInformationParser.restoreRouteInformation]
288/// method.
289///
290/// If the location in the new route information is different from the
291/// current location, this is considered to be a navigation event, the
292/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] method
293/// calls [SystemNavigator.routeInformationUpdated] with `replace = false` to
294/// notify the engine, and through that the browser, to create a history entry
295/// with the new url. Otherwise,
296/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] calls
297/// [SystemNavigator.routeInformationUpdated] with `replace = true` to update
298/// the current history entry with the latest [RouteInformation].
299///
300/// One can force the [Router] to report new route information as navigation
301/// event to the [routeInformationProvider] (and thus the browser) even if the
302/// [RouteInformation.uri] has not changed by calling the [Router.navigate]
303/// method with a callback that performs the state change. This causes [Router]
304/// to call the [RouteInformationProvider.routerReportsNewRouteInformation] with
305/// [RouteInformationReportingType.navigate], and thus causes
306/// [PlatformRouteInformationProvider] to push a new history entry regardlessly.
307/// This allows one to support the browser's back and forward buttons without
308/// changing the URL. For example, the scroll position of a scroll view may be
309/// saved in the [RouteInformation.state]. Using [Router.navigate] to update the
310/// scroll position causes the browser to create a new history entry with the
311/// [RouteInformation.state] that stores this new scroll position. When the user
312/// clicks the back button, the app will go back to the previous scroll position
313/// without changing the URL in the location bar.
314///
315/// One can also force the [Router] to ignore a navigation event by making
316/// those changes during a callback passed to [Router.neglect]. The [Router]
317/// calls the [RouteInformationProvider.routerReportsNewRouteInformation] with
318/// [RouteInformationReportingType.neglect], and thus causes
319/// [PlatformRouteInformationProvider] to replace the current history entry
320/// regardlessly even if it detects location change.
321///
322/// To opt out of URL updates entirely, pass null for [routeInformationProvider]
323/// and [routeInformationParser]. This is not recommended in general, but may be
324/// appropriate in the following cases:
325///
326/// * The application does not target the web platform.
327///
328/// * There are multiple router widgets in the application. Only one [Router]
329/// widget should update the URL (typically the top-most one created by the
330/// [WidgetsApp.router], [MaterialApp.router], or [CupertinoApp.router]).
331///
332/// * The application does not need to implement in-app navigation using the
333/// browser's back and forward buttons.
334///
335/// In other cases, it is strongly recommended to implement the
336/// [RouterDelegate.currentConfiguration] and
337/// [RouteInformationParser.restoreRouteInformation] APIs to provide an optimal
338/// user experience when running on the web platform.
339///
340/// ## State Restoration
341///
342/// The [Router] will restore the current configuration of the [routerDelegate]
343/// during state restoration if it is configured with a [restorationScopeId] and
344/// state restoration is enabled for the subtree. For that, the value of
345/// [RouterDelegate.currentConfiguration] is serialized and persisted before the
346/// app is killed by the operating system. After the app is restarted, the value
347/// is deserialized and passed back to the [RouterDelegate] via a call to
348/// [RouterDelegate.setRestoredRoutePath] (which by default just calls
349/// [RouterDelegate.setNewRoutePath]). It is the responsibility of the
350/// [RouterDelegate] to use the configuration information provided to restore
351/// its internal state.
352///
353/// To serialize [RouterDelegate.currentConfiguration] and to deserialize it
354/// again, the [Router] calls [RouteInformationParser.restoreRouteInformation]
355/// and [RouteInformationParser.parseRouteInformation], respectively. Therefore,
356/// if a [restorationScopeId] is provided, a [routeInformationParser] must be
357/// configured as well.
358class Router<T> extends StatefulWidget {
359 /// Creates a router.
360 ///
361 /// The [routeInformationProvider] and [routeInformationParser] can be null if this
362 /// router does not depend on route information. A common example is a sub router
363 /// that builds its content completely based on the app state.
364 ///
365 /// The [routeInformationProvider] and [routeInformationParser] must
366 /// both be provided or not provided.
367 const Router({
368 super.key,
369 this.routeInformationProvider,
370 this.routeInformationParser,
371 required this.routerDelegate,
372 this.backButtonDispatcher,
373 this.restorationScopeId,
374 }) : assert(
375 routeInformationProvider == null || routeInformationParser != null,
376 'A routeInformationParser must be provided when a routeInformationProvider is specified.',
377 );
378
379 /// Creates a router with a [RouterConfig].
380 ///
381 /// The [RouterConfig.routeInformationProvider] and
382 /// [RouterConfig.routeInformationParser] can be null if this router does not
383 /// depend on route information. A common example is a sub router that builds
384 /// its content completely based on the app state.
385 ///
386 /// If the [RouterConfig.routeInformationProvider] is not null, then
387 /// [RouterConfig.routeInformationParser] must also not be
388 /// null.
389 factory Router.withConfig({
390 Key? key,
391 required RouterConfig<T> config,
392 String? restorationScopeId,
393 }) {
394 return Router<T>(
395 key: key,
396 routeInformationProvider: config.routeInformationProvider,
397 routeInformationParser: config.routeInformationParser,
398 routerDelegate: config.routerDelegate,
399 backButtonDispatcher: config.backButtonDispatcher,
400 restorationScopeId: restorationScopeId,
401 );
402 }
403
404 /// The route information provider for the router.
405 ///
406 /// The value at the time of first build will be used as the initial route.
407 /// The [Router] listens to this provider and rebuilds with new names when
408 /// it notifies.
409 ///
410 /// This can be null if this router does not rely on the route information
411 /// to build its content. In such case, the [routeInformationParser] must also
412 /// be null.
413 final RouteInformationProvider? routeInformationProvider;
414
415 /// The route information parser for the router.
416 ///
417 /// When the [Router] gets a new route information from the [routeInformationProvider],
418 /// the [Router] uses this delegate to parse the route information and produce a
419 /// configuration. The configuration will be used by [routerDelegate] and
420 /// eventually rebuilds the [Router] widget.
421 ///
422 /// Since this delegate is the primary consumer of the [routeInformationProvider],
423 /// it must not be null if [routeInformationProvider] is not null.
424 final RouteInformationParser<T>? routeInformationParser;
425
426 /// The router delegate for the router.
427 ///
428 /// This delegate consumes the configuration from [routeInformationParser] and
429 /// builds a navigating widget for the [Router].
430 ///
431 /// It is also the primary respondent for the [backButtonDispatcher]. The
432 /// [Router] relies on [RouterDelegate.popRoute] to handle the back
433 /// button.
434 ///
435 /// If the [RouterDelegate.currentConfiguration] returns a non-null object,
436 /// this [Router] will opt for URL updates.
437 final RouterDelegate<T> routerDelegate;
438
439 /// The back button dispatcher for the router.
440 ///
441 /// The two common alternatives are the [RootBackButtonDispatcher] for root
442 /// router, or the [ChildBackButtonDispatcher] for other routers.
443 final BackButtonDispatcher? backButtonDispatcher;
444
445 /// Restoration ID to save and restore the state of the [Router].
446 ///
447 /// If non-null, the [Router] will persist the [RouterDelegate]'s current
448 /// configuration (i.e. [RouterDelegate.currentConfiguration]). During state
449 /// restoration, the [Router] informs the [RouterDelegate] of the previous
450 /// configuration by calling [RouterDelegate.setRestoredRoutePath] (which by
451 /// default just calls [RouterDelegate.setNewRoutePath]). It is the
452 /// responsibility of the [RouterDelegate] to restore its internal state based
453 /// on the provided configuration.
454 ///
455 /// The router uses the [RouteInformationParser] to serialize and deserialize
456 /// [RouterDelegate.currentConfiguration]. Therefore, a
457 /// [routeInformationParser] must be provided when [restorationScopeId] is
458 /// non-null.
459 ///
460 /// See also:
461 ///
462 /// * [RestorationManager], which explains how state restoration works in
463 /// Flutter.
464 final String? restorationScopeId;
465
466 /// Retrieves the immediate [Router] ancestor from the given context.
467 ///
468 /// This method provides access to the delegates in the [Router]. For example,
469 /// this can be used to access the [backButtonDispatcher] of the parent router
470 /// when creating a [ChildBackButtonDispatcher] for a nested [Router].
471 ///
472 /// If no [Router] ancestor exists for the given context, this will assert in
473 /// debug mode, and throw an exception in release mode.
474 ///
475 /// See also:
476 ///
477 /// * [maybeOf], which is a similar function, but it will return null instead
478 /// of throwing an exception if no [Router] ancestor exists.
479 static Router<T> of<T extends Object?>(BuildContext context) {
480 final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
481 assert(() {
482 if (scope == null) {
483 throw FlutterError(
484 'Router operation requested with a context that does not include a Router.\n'
485 'The context used to retrieve the Router must be that of a widget that '
486 'is a descendant of a Router widget.',
487 );
488 }
489 return true;
490 }());
491 return scope!.routerState.widget as Router<T>;
492 }
493
494 /// Retrieves the immediate [Router] ancestor from the given context.
495 ///
496 /// This method provides access to the delegates in the [Router]. For example,
497 /// this can be used to access the [backButtonDispatcher] of the parent router
498 /// when creating a [ChildBackButtonDispatcher] for a nested [Router].
499 ///
500 /// If no `Router` ancestor exists for the given context, this will return
501 /// null.
502 ///
503 /// See also:
504 ///
505 /// * [of], a similar method that returns a non-nullable value, and will
506 /// throw if no [Router] ancestor exists.
507 static Router<T>? maybeOf<T extends Object?>(BuildContext context) {
508 final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
509 return scope?.routerState.widget as Router<T>?;
510 }
511
512 /// Forces the [Router] to run the [callback] and create a new history
513 /// entry in the browser.
514 ///
515 /// The web application relies on the [Router] to report new route information
516 /// in order to create browser history entry. The [Router] will only report
517 /// them if it detects the [RouteInformation.uri] changes. Use this
518 /// method if you want the [Router] to report the route information even if
519 /// the location does not change. This can be useful when you want to
520 /// support the browser backward and forward button without changing the URL.
521 ///
522 /// For example, you can store certain state such as the scroll position into
523 /// the [RouteInformation.state]. If you use this method to update the
524 /// scroll position multiple times with the same URL, the browser will create
525 /// a stack of new history entries with the same URL but different
526 /// [RouteInformation.state]s that store the new scroll positions. If the user
527 /// click the backward button in the browser, the browser will restore the
528 /// scroll positions saved in history entries without changing the URL.
529 ///
530 /// See also:
531 ///
532 /// * [Router]: see the "URL updates for web applications" section for more
533 /// information about route information reporting.
534 /// * [neglect]: which forces the [Router] to not create a new history entry
535 /// even if location does change.
536 static void navigate(BuildContext context, VoidCallback callback) {
537 final _RouterScope scope =
538 context.getElementForInheritedWidgetOfExactType<_RouterScope>()!.widget as _RouterScope;
539 scope.routerState._setStateWithExplicitReportStatus(
540 RouteInformationReportingType.navigate,
541 callback,
542 );
543 }
544
545 /// Forces the [Router] to run the [callback] without creating a new history
546 /// entry in the browser.
547 ///
548 /// The web application relies on the [Router] to report new route information
549 /// in order to create browser history entry. The [Router] will report them
550 /// automatically if it detects the [RouteInformation.uri] changes.
551 ///
552 /// Creating a new route history entry makes users feel they have visited a
553 /// new page, and the browser back button brings them back to previous history
554 /// entry. Use this method if you don't want the [Router] to create a new
555 /// route information even if it detects changes as a result of running the
556 /// [callback].
557 ///
558 /// Using this method will still update the URL and state in current history
559 /// entry.
560 ///
561 /// See also:
562 ///
563 /// * [Router]: see the "URL updates for web applications" section for more
564 /// information about route information reporting.
565 /// * [navigate]: which forces the [Router] to create a new history entry
566 /// even if location does not change.
567 static void neglect(BuildContext context, VoidCallback callback) {
568 final _RouterScope scope =
569 context.getElementForInheritedWidgetOfExactType<_RouterScope>()!.widget as _RouterScope;
570 scope.routerState._setStateWithExplicitReportStatus(
571 RouteInformationReportingType.neglect,
572 callback,
573 );
574 }
575
576 @override
577 State<Router<T>> createState() => _RouterState<T>();
578}
579
580typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
581typedef _RouteSetter<T> = Future<void> Function(T);
582
583/// The [Router]'s intention when it reports a new [RouteInformation] to the
584/// [RouteInformationProvider].
585///
586/// See also:
587///
588/// * [RouteInformationProvider.routerReportsNewRouteInformation]: which is
589/// called by the router when it has a new route information to report.
590enum RouteInformationReportingType {
591 /// Router does not have a specific intention.
592 ///
593 /// The router generates a new route information every time it detects route
594 /// information may have change due to a rebuild. This is the default type if
595 /// neither [Router.neglect] nor [Router.navigate] was used during the
596 /// rebuild.
597 none,
598
599 /// The accompanying [RouteInformation] were generated during a
600 /// [Router.neglect] call.
601 neglect,
602
603 /// The accompanying [RouteInformation] were generated during a
604 /// [Router.navigate] call.
605 navigate,
606}
607
608class _RouterState<T> extends State<Router<T>> with RestorationMixin {
609 Object? _currentRouterTransaction;
610 RouteInformationReportingType? _currentIntentionToReport;
611 final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
612 late bool _routeParsePending;
613
614 @override
615 String? get restorationId => widget.restorationScopeId;
616
617 @override
618 void initState() {
619 super.initState();
620 widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
621 widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
622 widget.routerDelegate.addListener(_handleRouterDelegateNotification);
623 }
624
625 @override
626 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
627 registerForRestoration(_routeInformation, 'route');
628 if (_routeInformation.value != null) {
629 assert(widget.routeInformationParser != null);
630 _processRouteInformation(
631 _routeInformation.value!,
632 () => widget.routerDelegate.setRestoredRoutePath,
633 );
634 } else if (widget.routeInformationProvider != null) {
635 _processRouteInformation(
636 widget.routeInformationProvider!.value,
637 () => widget.routerDelegate.setInitialRoutePath,
638 );
639 }
640 }
641
642 bool _routeInformationReportingTaskScheduled = false;
643
644 void _scheduleRouteInformationReportingTask() {
645 if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null) {
646 return;
647 }
648 assert(_currentIntentionToReport != null);
649 _routeInformationReportingTaskScheduled = true;
650 SchedulerBinding.instance.addPostFrameCallback(
651 _reportRouteInformation,
652 debugLabel: 'Router.reportRouteInfo',
653 );
654 }
655
656 void _reportRouteInformation(Duration timestamp) {
657 if (!mounted) {
658 return;
659 }
660
661 assert(_routeInformationReportingTaskScheduled);
662 _routeInformationReportingTaskScheduled = false;
663
664 if (_routeInformation.value != null) {
665 final RouteInformation currentRouteInformation = _routeInformation.value!;
666 assert(_currentIntentionToReport != null);
667 widget.routeInformationProvider!.routerReportsNewRouteInformation(
668 currentRouteInformation,
669 type: _currentIntentionToReport!,
670 );
671 }
672 _currentIntentionToReport = RouteInformationReportingType.none;
673 }
674
675 RouteInformation? _retrieveNewRouteInformation() {
676 final T? configuration = widget.routerDelegate.currentConfiguration;
677 if (configuration == null) {
678 return null;
679 }
680 return widget.routeInformationParser?.restoreRouteInformation(configuration);
681 }
682
683 void _setStateWithExplicitReportStatus(RouteInformationReportingType status, VoidCallback fn) {
684 assert(status.index >= RouteInformationReportingType.neglect.index);
685 assert(() {
686 if (_currentIntentionToReport != null &&
687 _currentIntentionToReport != RouteInformationReportingType.none &&
688 _currentIntentionToReport != status) {
689 FlutterError.reportError(
690 const FlutterErrorDetails(
691 exception:
692 'Both Router.navigate and Router.neglect have been called in this '
693 'build cycle, and the Router cannot decide whether to report the '
694 'route information. Please make sure only one of them is called '
695 'within the same build cycle.',
696 ),
697 );
698 }
699 return true;
700 }());
701 _currentIntentionToReport = status;
702 _scheduleRouteInformationReportingTask();
703 fn();
704 }
705
706 void _maybeNeedToReportRouteInformation() {
707 _routeInformation.value = _retrieveNewRouteInformation();
708 _currentIntentionToReport ??= RouteInformationReportingType.none;
709 _scheduleRouteInformationReportingTask();
710 }
711
712 @override
713 void didChangeDependencies() {
714 _routeParsePending = true;
715 super.didChangeDependencies();
716 // The super.didChangeDependencies may have parsed the route information.
717 // This can happen if the didChangeDependencies is triggered by state
718 // restoration or first build.
719 final RouteInformation? currentRouteInformation =
720 _routeInformation.value ?? widget.routeInformationProvider?.value;
721 if (currentRouteInformation != null && _routeParsePending) {
722 _processRouteInformation(
723 currentRouteInformation,
724 () => widget.routerDelegate.setNewRoutePath,
725 );
726 }
727 _routeParsePending = false;
728 _maybeNeedToReportRouteInformation();
729 }
730
731 @override
732 void didUpdateWidget(Router<T> oldWidget) {
733 super.didUpdateWidget(oldWidget);
734 if (widget.routeInformationProvider != oldWidget.routeInformationProvider ||
735 widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
736 widget.routeInformationParser != oldWidget.routeInformationParser ||
737 widget.routerDelegate != oldWidget.routerDelegate) {
738 _currentRouterTransaction = Object();
739 }
740 if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
741 oldWidget.routeInformationProvider?.removeListener(
742 _handleRouteInformationProviderNotification,
743 );
744 widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
745 if (oldWidget.routeInformationProvider?.value != widget.routeInformationProvider?.value) {
746 _handleRouteInformationProviderNotification();
747 }
748 }
749 if (widget.backButtonDispatcher != oldWidget.backButtonDispatcher) {
750 oldWidget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
751 widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
752 }
753 if (widget.routerDelegate != oldWidget.routerDelegate) {
754 oldWidget.routerDelegate.removeListener(_handleRouterDelegateNotification);
755 widget.routerDelegate.addListener(_handleRouterDelegateNotification);
756 _maybeNeedToReportRouteInformation();
757 }
758 }
759
760 @override
761 void dispose() {
762 _routeInformation.dispose();
763 widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
764 widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
765 widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
766 _currentRouterTransaction = null;
767 super.dispose();
768 }
769
770 void _processRouteInformation(
771 RouteInformation information,
772 ValueGetter<_RouteSetter<T>> delegateRouteSetter,
773 ) {
774 assert(_routeParsePending);
775 _routeParsePending = false;
776 _currentRouterTransaction = Object();
777 widget.routeInformationParser!
778 .parseRouteInformationWithDependencies(information, context)
779 .then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
780 }
781
782 _RouteSetter<T> _processParsedRouteInformation(
783 Object? transaction,
784 ValueGetter<_RouteSetter<T>> delegateRouteSetter,
785 ) {
786 return (T data) async {
787 if (_currentRouterTransaction != transaction) {
788 return;
789 }
790 await delegateRouteSetter()(data);
791 if (_currentRouterTransaction == transaction) {
792 _rebuild();
793 }
794 };
795 }
796
797 void _handleRouteInformationProviderNotification() {
798 _routeParsePending = true;
799 _processRouteInformation(
800 widget.routeInformationProvider!.value,
801 () => widget.routerDelegate.setNewRoutePath,
802 );
803 }
804
805 Future<bool> _handleBackButtonDispatcherNotification() {
806 _currentRouterTransaction = Object();
807 return widget.routerDelegate.popRoute().then<bool>(
808 _handleRoutePopped(_currentRouterTransaction),
809 );
810 }
811
812 _AsyncPassthrough<bool> _handleRoutePopped(Object? transaction) {
813 return (bool data) {
814 if (transaction != _currentRouterTransaction) {
815 // A rebuilt was trigger from a different source. Returns true to
816 // prevent bubbling.
817 return SynchronousFuture<bool>(true);
818 }
819 _rebuild();
820 return SynchronousFuture<bool>(data);
821 };
822 }
823
824 Future<void> _rebuild([void value]) {
825 setState(() {
826 /* routerDelegate is ready to rebuild */
827 });
828 _maybeNeedToReportRouteInformation();
829 return SynchronousFuture<void>(value);
830 }
831
832 void _handleRouterDelegateNotification() {
833 setState(() {
834 /* routerDelegate wants to rebuild */
835 });
836 _maybeNeedToReportRouteInformation();
837 }
838
839 @override
840 Widget build(BuildContext context) {
841 return UnmanagedRestorationScope(
842 bucket: bucket,
843 child: _RouterScope(
844 routeInformationProvider: widget.routeInformationProvider,
845 backButtonDispatcher: widget.backButtonDispatcher,
846 routeInformationParser: widget.routeInformationParser,
847 routerDelegate: widget.routerDelegate,
848 routerState: this,
849 child: Builder(
850 // Use a Builder so that the build method below will have a
851 // BuildContext that contains the _RouterScope. This also prevents
852 // dependencies look ups in routerDelegate from rebuilding Router
853 // widget that may result in re-parsing the route information.
854 builder: widget.routerDelegate.build,
855 ),
856 ),
857 );
858 }
859}
860
861class _RouterScope extends InheritedWidget {
862 const _RouterScope({
863 required this.routeInformationProvider,
864 required this.backButtonDispatcher,
865 required this.routeInformationParser,
866 required this.routerDelegate,
867 required this.routerState,
868 required super.child,
869 }) : assert(routeInformationProvider == null || routeInformationParser != null);
870
871 final ValueListenable<RouteInformation?>? routeInformationProvider;
872 final BackButtonDispatcher? backButtonDispatcher;
873 final RouteInformationParser<Object?>? routeInformationParser;
874 final RouterDelegate<Object?> routerDelegate;
875 final _RouterState<Object?> routerState;
876
877 @override
878 bool updateShouldNotify(_RouterScope oldWidget) {
879 return routeInformationProvider != oldWidget.routeInformationProvider ||
880 backButtonDispatcher != oldWidget.backButtonDispatcher ||
881 routeInformationParser != oldWidget.routeInformationParser ||
882 routerDelegate != oldWidget.routerDelegate ||
883 routerState != oldWidget.routerState;
884 }
885}
886
887/// A class that can be extended or mixed in that invokes a single callback,
888/// which then returns a value.
889///
890/// While multiple callbacks can be registered, when a notification is
891/// dispatched there must be only a single callback. The return values of
892/// multiple callbacks are not aggregated.
893///
894/// `T` is the return value expected from the callback.
895///
896/// See also:
897///
898/// * [Listenable] and its subclasses, which provide a similar mechanism for
899/// one-way signaling.
900class _CallbackHookProvider<T> {
901 final ObserverList<ValueGetter<T>> _callbacks = ObserverList<ValueGetter<T>>();
902
903 /// Whether a callback is currently registered.
904 @protected
905 bool get hasCallbacks => _callbacks.isNotEmpty;
906
907 /// Register the callback to be called when the object changes.
908 ///
909 /// If other callbacks have already been registered, they must be removed
910 /// (with [removeCallback]) before the callback is next called.
911 void addCallback(ValueGetter<T> callback) => _callbacks.add(callback);
912
913 /// Remove a previously registered callback.
914 ///
915 /// If the given callback is not registered, the call is ignored.
916 void removeCallback(ValueGetter<T> callback) => _callbacks.remove(callback);
917
918 /// Calls the (single) registered callback and returns its result.
919 ///
920 /// If no callback is registered, or if the callback throws, returns
921 /// `defaultValue`.
922 ///
923 /// Call this method whenever the callback is to be invoked. If there is more
924 /// than one callback registered, this method will throw a [StateError].
925 ///
926 /// Exceptions thrown by callbacks will be caught and reported using
927 /// [FlutterError.reportError].
928 @protected
929 @pragma('vm:notify-debugger-on-exception')
930 T invokeCallback(T defaultValue) {
931 if (_callbacks.isEmpty) {
932 return defaultValue;
933 }
934 try {
935 return _callbacks.single();
936 } catch (exception, stack) {
937 FlutterError.reportError(
938 FlutterErrorDetails(
939 exception: exception,
940 stack: stack,
941 library: 'widget library',
942 context: ErrorDescription('while invoking the callback for $runtimeType'),
943 informationCollector:
944 () => <DiagnosticsNode>[
945 DiagnosticsProperty<_CallbackHookProvider<T>>(
946 'The $runtimeType that invoked the callback was',
947 this,
948 style: DiagnosticsTreeStyle.errorProperty,
949 ),
950 ],
951 ),
952 );
953 return defaultValue;
954 }
955 }
956}
957
958/// Report to a [Router] when the user taps the back button on platforms that
959/// support back buttons (such as Android).
960///
961/// When [Router] widgets are nested, consider using a
962/// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher],
963/// so that the back button requests get dispatched to the appropriate [Router].
964/// To make this work properly, it's important that whenever a [Router] thinks
965/// it should get the back button messages (e.g. after the user taps inside it),
966/// it calls [takePriority] on its [BackButtonDispatcher] (or
967/// [ChildBackButtonDispatcher]) instance.
968///
969/// The class takes a single callback, which must return a [Future<bool>]. The
970/// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely,
971/// the callback should return a future that completes to true if it can handle
972/// the pop request, and a future that completes to false otherwise.
973abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
974 late final LinkedHashSet<ChildBackButtonDispatcher> _children =
975 <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
976
977 @override
978 bool get hasCallbacks => super.hasCallbacks || (_children.isNotEmpty);
979
980 /// Handles a pop route request.
981 ///
982 /// This method prioritizes the children list in reverse order and calls
983 /// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them
984 /// handles the request (by returning a future with true), it exits this
985 /// method by returning this future. Otherwise, it keeps moving on to the next
986 /// child until a child handles the request. If none of the children handles
987 /// the request, this back button dispatcher will then try to handle the request
988 /// by itself. This back button dispatcher handles the request by notifying the
989 /// router which in turn calls the [RouterDelegate.popRoute] and returns its
990 /// result.
991 ///
992 /// To decide whether this back button dispatcher will handle the pop route
993 /// request, you can override the [RouterDelegate.popRoute] of the router
994 /// delegate you pass into the router with this back button dispatcher to
995 /// return a future of true or false.
996 @override
997 Future<bool> invokeCallback(Future<bool> defaultValue) {
998 if (_children.isNotEmpty) {
999 final List<ChildBackButtonDispatcher> children = _children.toList();
1000 int childIndex = children.length - 1;
1001
1002 Future<bool> notifyNextChild(bool result) {
1003 // If the previous child handles the callback, we return the result.
1004 if (result) {
1005 return SynchronousFuture<bool>(result);
1006 }
1007 // If the previous child did not handle the callback, we ask the next
1008 // child to handle the it.
1009 if (childIndex > 0) {
1010 childIndex -= 1;
1011 return children[childIndex].notifiedByParent(defaultValue).then<bool>(notifyNextChild);
1012 }
1013 // If none of the child handles the callback, the parent will then handle it.
1014 return super.invokeCallback(defaultValue);
1015 }
1016
1017 return children[childIndex].notifiedByParent(defaultValue).then<bool>(notifyNextChild);
1018 }
1019 return super.invokeCallback(defaultValue);
1020 }
1021
1022 /// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this
1023 /// back button dispatcher.
1024 ///
1025 /// To participate in handling the pop route request, call the [takePriority]
1026 /// on the [ChildBackButtonDispatcher] created from this method.
1027 ///
1028 /// When the pop route request is handled by this back button dispatcher, it
1029 /// propagate the request to its direct descendants that have called the
1030 /// [takePriority] method. If there are multiple candidates, the latest one
1031 /// that called the [takePriority] wins the right to handle the request. If
1032 /// the latest one does not handle the request (by returning a future of
1033 /// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest
1034 /// one will then have the right to handle the request. This dispatcher
1035 /// continues finding the next candidate until there are no more candidates
1036 /// and finally handles the request itself.
1037 ChildBackButtonDispatcher createChildBackButtonDispatcher() {
1038 return ChildBackButtonDispatcher(this);
1039 }
1040
1041 /// Make this [BackButtonDispatcher] take priority among its peers.
1042 ///
1043 /// This has no effect when a [BackButtonDispatcher] has no parents and no
1044 /// children. If a [BackButtonDispatcher] does have parents or children,
1045 /// however, it causes this object to be the one to dispatch the notification
1046 /// when the parent would normally notify its callback.
1047 ///
1048 /// The [BackButtonDispatcher] must have a listener registered before it can
1049 /// be told to take priority.
1050 void takePriority() => _children.clear();
1051
1052 /// Mark the given child as taking priority over this object and the other
1053 /// children.
1054 ///
1055 /// This causes [invokeCallback] to defer to the given child instead of
1056 /// calling this object's callback.
1057 ///
1058 /// Children are stored in a list, so that if the current child is removed
1059 /// using [forget], a previous child will return to take its place. When
1060 /// [takePriority] is called, the list is cleared.
1061 ///
1062 /// Calling this again without first calling [forget] moves the child back to
1063 /// the head of the list.
1064 ///
1065 /// The [BackButtonDispatcher] must have a listener registered before it can
1066 /// be told to defer to a child.
1067 void deferTo(ChildBackButtonDispatcher child) {
1068 assert(hasCallbacks);
1069 _children.remove(child); // child may or may not be in the set already
1070 _children.add(child);
1071 }
1072
1073 /// Causes the given child to be removed from the list of children to which
1074 /// this object might defer, as if [deferTo] had never been called for that
1075 /// child.
1076 ///
1077 /// This should only be called once per child, even if [deferTo] was called
1078 /// multiple times for that child.
1079 ///
1080 /// If no children are left in the list, this object will stop deferring to
1081 /// its children. (This is not the same as calling [takePriority], since, if
1082 /// this object itself is a [ChildBackButtonDispatcher], [takePriority] would
1083 /// additionally attempt to claim priority from its parent, whereas removing
1084 /// the last child does not.)
1085 void forget(ChildBackButtonDispatcher child) => _children.remove(child);
1086}
1087
1088/// The default implementation of back button dispatcher for the root router.
1089///
1090/// This dispatcher listens to platform pop route notifications. When the
1091/// platform wants to pop the current route, this dispatcher calls the
1092/// [BackButtonDispatcher.invokeCallback] method to handle the request.
1093class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingObserver {
1094 /// Create a root back button dispatcher.
1095 RootBackButtonDispatcher();
1096
1097 @override
1098 void addCallback(ValueGetter<Future<bool>> callback) {
1099 if (!hasCallbacks) {
1100 WidgetsBinding.instance.addObserver(this);
1101 }
1102 super.addCallback(callback);
1103 }
1104
1105 @override
1106 void removeCallback(ValueGetter<Future<bool>> callback) {
1107 super.removeCallback(callback);
1108 if (!hasCallbacks) {
1109 WidgetsBinding.instance.removeObserver(this);
1110 }
1111 }
1112
1113 @override
1114 Future<bool> didPopRoute() => invokeCallback(Future<bool>.value(false));
1115}
1116
1117/// A variant of [BackButtonDispatcher] which listens to notifications from a
1118/// parent back button dispatcher, and can take priority from its parent for the
1119/// handling of such notifications.
1120///
1121/// Useful when [Router]s are being nested within each other.
1122///
1123/// Use [Router.of] to obtain a reference to the nearest ancestor [Router], from
1124/// which the [Router.backButtonDispatcher] can be found, and then used as the
1125/// [parent] of the [ChildBackButtonDispatcher].
1126class ChildBackButtonDispatcher extends BackButtonDispatcher {
1127 /// Creates a back button dispatcher that acts as the child of another.
1128 ChildBackButtonDispatcher(this.parent);
1129
1130 /// The back button dispatcher that this object will attempt to take priority
1131 /// over when [takePriority] is called.
1132 ///
1133 /// The parent must have a listener registered before this child object can
1134 /// have its [takePriority] or [deferTo] methods used.
1135 final BackButtonDispatcher parent;
1136
1137 /// The parent of this child back button dispatcher decide to let this
1138 /// child to handle the invoke the callback request in
1139 /// [BackButtonDispatcher.invokeCallback].
1140 ///
1141 /// Return a boolean future with true if this child will handle the request;
1142 /// otherwise, return a boolean future with false.
1143 @protected
1144 Future<bool> notifiedByParent(Future<bool> defaultValue) {
1145 return invokeCallback(defaultValue);
1146 }
1147
1148 @override
1149 void takePriority() {
1150 parent.deferTo(this);
1151 super.takePriority();
1152 }
1153
1154 @override
1155 void deferTo(ChildBackButtonDispatcher child) {
1156 assert(hasCallbacks);
1157 parent.deferTo(this);
1158 super.deferTo(child);
1159 }
1160
1161 @override
1162 void removeCallback(ValueGetter<Future<bool>> callback) {
1163 super.removeCallback(callback);
1164 if (!hasCallbacks) {
1165 parent.forget(this);
1166 }
1167 }
1168}
1169
1170/// A convenience widget that registers a callback for when the back button is pressed.
1171///
1172/// In order to use this widget, there must be an ancestor [Router] widget in the tree
1173/// that has a [RootBackButtonDispatcher]. e.g. The [Router] widget created by the
1174/// [MaterialApp.router] has a built-in [RootBackButtonDispatcher] by default.
1175///
1176/// It only applies to platforms that accept back button clicks, such as Android.
1177///
1178/// It can be useful for scenarios, in which you create a different state in your
1179/// screen but don't want to use a new page for that.
1180class BackButtonListener extends StatefulWidget {
1181 /// Creates a BackButtonListener widget .
1182 const BackButtonListener({super.key, required this.child, required this.onBackButtonPressed});
1183
1184 /// The widget below this widget in the tree.
1185 final Widget child;
1186
1187 /// The callback function that will be called when the back button is pressed.
1188 ///
1189 /// It must return a boolean future with true if this child will handle the request;
1190 /// otherwise, return a boolean future with false.
1191 final ValueGetter<Future<bool>> onBackButtonPressed;
1192
1193 @override
1194 State<BackButtonListener> createState() => _BackButtonListenerState();
1195}
1196
1197class _BackButtonListenerState extends State<BackButtonListener> {
1198 BackButtonDispatcher? dispatcher;
1199
1200 @override
1201 void didChangeDependencies() {
1202 dispatcher?.removeCallback(widget.onBackButtonPressed);
1203
1204 final BackButtonDispatcher? rootBackDispatcher = Router.of(context).backButtonDispatcher;
1205 assert(
1206 rootBackDispatcher != null,
1207 'The parent router must have a backButtonDispatcher to use this widget',
1208 );
1209
1210 dispatcher =
1211 rootBackDispatcher!.createChildBackButtonDispatcher()
1212 ..addCallback(widget.onBackButtonPressed)
1213 ..takePriority();
1214 super.didChangeDependencies();
1215 }
1216
1217 @override
1218 void didUpdateWidget(covariant BackButtonListener oldWidget) {
1219 super.didUpdateWidget(oldWidget);
1220 if (oldWidget.onBackButtonPressed != widget.onBackButtonPressed) {
1221 dispatcher?.removeCallback(oldWidget.onBackButtonPressed);
1222 dispatcher?.addCallback(widget.onBackButtonPressed);
1223 dispatcher?.takePriority();
1224 }
1225 }
1226
1227 @override
1228 void dispose() {
1229 dispatcher?.removeCallback(widget.onBackButtonPressed);
1230 super.dispose();
1231 }
1232
1233 @override
1234 Widget build(BuildContext context) => widget.child;
1235}
1236
1237/// A delegate that is used by the [Router] widget to parse a route information
1238/// into a configuration of type T.
1239///
1240/// This delegate is used when the [Router] widget is first built with initial
1241/// route information from [Router.routeInformationProvider] and any subsequent
1242/// new route notifications from it. The [Router] widget calls the [parseRouteInformation]
1243/// with the route information from [Router.routeInformationProvider].
1244///
1245/// One of the [parseRouteInformation] or
1246/// [parseRouteInformationWithDependencies] must be implemented, otherwise a
1247/// runtime error will be thrown.
1248abstract class RouteInformationParser<T> {
1249 /// Abstract const constructor. This constructor enables subclasses to provide
1250 /// const constructors so that they can be used in const expressions.
1251 const RouteInformationParser();
1252
1253 /// {@template flutter.widgets.RouteInformationParser.parseRouteInformation}
1254 /// Converts the given route information into parsed data to pass to a
1255 /// [RouterDelegate].
1256 ///
1257 /// The method should return a future which completes when the parsing is
1258 /// complete. The parsing may be asynchronous if, e.g., the parser needs to
1259 /// communicate with the OEM thread to obtain additional data about the route.
1260 ///
1261 /// Consider using a [SynchronousFuture] if the result can be computed
1262 /// synchronously, so that the [Router] does not need to wait for the next
1263 /// microtask to pass the data to the [RouterDelegate].
1264 /// {@endtemplate}
1265 ///
1266 /// One can implement [parseRouteInformationWithDependencies] instead if
1267 /// the parsing depends on other dependencies from the [BuildContext].
1268 Future<T> parseRouteInformation(RouteInformation routeInformation) {
1269 throw UnimplementedError(
1270 'One of the parseRouteInformation or '
1271 'parseRouteInformationWithDependencies must be implemented',
1272 );
1273 }
1274
1275 /// {@macro flutter.widgets.RouteInformationParser.parseRouteInformation}
1276 ///
1277 /// The input [BuildContext] can be used for looking up [InheritedWidget]s
1278 /// If one uses [BuildContext.dependOnInheritedWidgetOfExactType], a
1279 /// dependency will be created. The [Router] will re-parse the
1280 /// [RouteInformation] from its [RouteInformationProvider] if the dependency
1281 /// notifies its listeners.
1282 ///
1283 /// One can also use [BuildContext.getElementForInheritedWidgetOfExactType] to
1284 /// look up [InheritedWidget]s without creating dependencies.
1285 Future<T> parseRouteInformationWithDependencies(
1286 RouteInformation routeInformation,
1287 BuildContext context,
1288 ) {
1289 return parseRouteInformation(routeInformation);
1290 }
1291
1292 /// Restore the route information from the given configuration.
1293 ///
1294 /// This may return null, in which case the browser history will not be
1295 /// updated and state restoration is disabled. See [Router]'s documentation
1296 /// for details.
1297 ///
1298 /// The [parseRouteInformation] method must produce an equivalent
1299 /// configuration when passed this method's return value.
1300 RouteInformation? restoreRouteInformation(T configuration) => null;
1301}
1302
1303/// A delegate that is used by the [Router] widget to build and configure a
1304/// navigating widget.
1305///
1306/// This delegate is the core piece of the [Router] widget. It responds to
1307/// push route and pop route intents from the engine and notifies the [Router]
1308/// to rebuild. It also acts as a builder for the [Router] widget and builds a
1309/// navigating widget, typically a [Navigator], when the [Router] widget
1310/// builds.
1311///
1312/// When the engine pushes a new route, the route information is parsed by the
1313/// [RouteInformationParser] to produce a configuration of type T. The router
1314/// delegate receives the configuration through [setInitialRoutePath] or
1315/// [setNewRoutePath] to configure itself and builds the latest navigating
1316/// widget when asked ([build]).
1317///
1318/// When implementing subclasses, consider defining a [Listenable] app state object to be
1319/// used for building the navigating widget. The router delegate would update
1320/// the app state accordingly and notify its own listeners when the app state has
1321/// changed and when it receive route related engine intents (e.g.
1322/// [setNewRoutePath], [setInitialRoutePath], or [popRoute]).
1323///
1324/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
1325///
1326/// ## State Restoration
1327///
1328/// If the [Router] owning this delegate is configured for state restoration, it
1329/// will persist and restore the configuration of this [RouterDelegate] using
1330/// the following mechanism: Before the app is killed by the operating system,
1331/// the value of [currentConfiguration] is serialized out and persisted. After
1332/// the app has restarted, the value is deserialized and passed back to the
1333/// [RouterDelegate] via a call to [setRestoredRoutePath] (which by default just
1334/// calls [setNewRoutePath]). It is the responsibility of the [RouterDelegate]
1335/// to use the configuration information provided to restore its internal state.
1336///
1337/// See also:
1338///
1339/// * [RouteInformationParser], which is responsible for parsing the route
1340/// information to a configuration before passing in to router delegate.
1341/// * [Router], which is the widget that wires all the delegates together to
1342/// provide a fully functional routing solution.
1343abstract class RouterDelegate<T> extends Listenable {
1344 /// Called by the [Router] at startup with the structure that the
1345 /// [RouteInformationParser] obtained from parsing the initial route.
1346 ///
1347 /// This should configure the [RouterDelegate] so that when [build] is
1348 /// invoked, it will create a widget tree that matches the initial route.
1349 ///
1350 /// By default, this method forwards the [configuration] to [setNewRoutePath].
1351 ///
1352 /// Consider using a [SynchronousFuture] if the result can be computed
1353 /// synchronously, so that the [Router] does not need to wait for the next
1354 /// microtask to schedule a build.
1355 ///
1356 /// See also:
1357 ///
1358 /// * [setRestoredRoutePath], which is called instead of this method during
1359 /// state restoration.
1360 Future<void> setInitialRoutePath(T configuration) {
1361 return setNewRoutePath(configuration);
1362 }
1363
1364 /// Called by the [Router] during state restoration.
1365 ///
1366 /// When the [Router] is configured for state restoration, it will persist
1367 /// the value of [currentConfiguration] during state serialization. During
1368 /// state restoration, the [Router] calls this method (instead of
1369 /// [setInitialRoutePath]) to pass the previous configuration back to the
1370 /// delegate. It is the responsibility of the delegate to restore its internal
1371 /// state based on the provided configuration.
1372 ///
1373 /// By default, this method forwards the `configuration` to [setNewRoutePath].
1374 Future<void> setRestoredRoutePath(T configuration) {
1375 return setNewRoutePath(configuration);
1376 }
1377
1378 /// Called by the [Router] when the [Router.routeInformationProvider] reports that a
1379 /// new route has been pushed to the application by the operating system.
1380 ///
1381 /// Consider using a [SynchronousFuture] if the result can be computed
1382 /// synchronously, so that the [Router] does not need to wait for the next
1383 /// microtask to schedule a build.
1384 Future<void> setNewRoutePath(T configuration);
1385
1386 /// Called by the [Router] when the [Router.backButtonDispatcher] reports that
1387 /// the operating system is requesting that the current route be popped.
1388 ///
1389 /// The method should return a boolean [Future] to indicate whether this
1390 /// delegate handles the request. Returning false will cause the entire app
1391 /// to be popped.
1392 ///
1393 /// Consider using a [SynchronousFuture] if the result can be computed
1394 /// synchronously, so that the [Router] does not need to wait for the next
1395 /// microtask to schedule a build.
1396 Future<bool> popRoute();
1397
1398 /// Called by the [Router] when it detects a route information may have
1399 /// changed as a result of rebuild.
1400 ///
1401 /// If this getter returns non-null, the [Router] will start to report new
1402 /// route information back to the engine. In web applications, the new
1403 /// route information is used for populating browser history in order to
1404 /// support the forward and the backward buttons.
1405 ///
1406 /// When overriding this method, the configuration returned by this getter
1407 /// must be able to construct the current app state and build the widget
1408 /// with the same configuration in the [build] method if it is passed back
1409 /// to the [setNewRoutePath]. Otherwise, the browser backward and forward
1410 /// buttons will not work properly.
1411 ///
1412 /// By default, this getter returns null, which prevents the [Router] from
1413 /// reporting the route information. To opt in, a subclass can override this
1414 /// getter to return the current configuration.
1415 ///
1416 /// At most one [Router] can opt in to route information reporting. Typically,
1417 /// only the top-most [Router] created by [WidgetsApp.router] should opt for
1418 /// route information reporting.
1419 ///
1420 /// ## State Restoration
1421 ///
1422 /// This getter is also used by the [Router] to implement state restoration.
1423 /// During state serialization, the [Router] will persist the current
1424 /// configuration and during state restoration pass it back to the delegate
1425 /// by calling [setRestoredRoutePath].
1426 T? get currentConfiguration => null;
1427
1428 /// Called by the [Router] to obtain the widget tree that represents the
1429 /// current state.
1430 ///
1431 /// This is called whenever the [Future]s returned by [setInitialRoutePath],
1432 /// [setNewRoutePath], or [setRestoredRoutePath] complete as well as when this
1433 /// notifies its clients (see the [Listenable] interface, which this interface
1434 /// includes). In addition, it may be called at other times. It is important,
1435 /// therefore, that the methods above do not update the state that the [build]
1436 /// method uses before they complete their respective futures.
1437 ///
1438 /// Typically this method returns a suitably-configured [Navigator]. If you do
1439 /// plan to create a navigator, consider using the
1440 /// [PopNavigatorRouterDelegateMixin]. If state restoration is enabled for the
1441 /// [Router] using this delegate, consider providing a non-null
1442 /// [Navigator.restorationScopeId] to the [Navigator] returned by this method.
1443 ///
1444 /// This method must not return null.
1445 ///
1446 /// The `context` is the [Router]'s build context.
1447 Widget build(BuildContext context);
1448}
1449
1450/// A route information provider that provides route information for the
1451/// [Router] widget
1452///
1453/// This provider is responsible for handing the route information through [value]
1454/// getter and notifies listeners, typically the [Router] widget, when a new
1455/// route information is available.
1456///
1457/// When the router opts for route information reporting (by overriding the
1458/// [RouterDelegate.currentConfiguration] to return non-null), override the
1459/// [routerReportsNewRouteInformation] method to process the route information.
1460///
1461/// See also:
1462///
1463/// * [PlatformRouteInformationProvider], which wires up the itself with the
1464/// [WidgetsBindingObserver.didPushRoute] to propagate platform push route
1465/// intent to the [Router] widget, as well as reports new route information
1466/// from the [Router] back to the engine by overriding the
1467/// [routerReportsNewRouteInformation].
1468abstract class RouteInformationProvider extends ValueListenable<RouteInformation> {
1469 /// A callback called when the [Router] widget reports new route information
1470 ///
1471 /// The subclasses can override this method to update theirs values or trigger
1472 /// other side effects. For example, the [PlatformRouteInformationProvider]
1473 /// overrides this method to report the route information back to the engine.
1474 ///
1475 /// The `routeInformation` is the new route information generated by the
1476 /// Router rebuild, and it can be the same or different from the
1477 /// [value].
1478 ///
1479 /// The `type` denotes the [Router]'s intention when it reports this
1480 /// `routeInformation`. It is useful when deciding how to update the internal
1481 /// state of [RouteInformationProvider] subclass with the `routeInformation`.
1482 /// For example, [PlatformRouteInformationProvider] uses this property to
1483 /// decide whether to push or replace the browser history entry with the new
1484 /// `routeInformation`.
1485 ///
1486 /// For more information on how [Router] determines a navigation event, see
1487 /// the "URL updates for web applications" section in the [Router]
1488 /// documentation.
1489 void routerReportsNewRouteInformation(
1490 RouteInformation routeInformation, {
1491 RouteInformationReportingType type = RouteInformationReportingType.none,
1492 }) {}
1493}
1494
1495/// The route information provider that propagates the platform route information changes.
1496///
1497/// This provider also reports the new route information from the [Router] widget
1498/// back to engine using message channel method, the
1499/// [SystemNavigator.routeInformationUpdated].
1500///
1501/// Each time [SystemNavigator.routeInformationUpdated] is called, the
1502/// [SystemNavigator.selectMultiEntryHistory] method is also called. This
1503/// overrides the initialization behavior of
1504/// [Navigator.reportsRouteUpdateToEngine].
1505class PlatformRouteInformationProvider extends RouteInformationProvider
1506 with WidgetsBindingObserver, ChangeNotifier {
1507 /// Create a platform route information provider.
1508 ///
1509 /// Use the [initialRouteInformation] to set the default route information for this
1510 /// provider.
1511 PlatformRouteInformationProvider({required RouteInformation initialRouteInformation})
1512 : _value = initialRouteInformation {
1513 if (kFlutterMemoryAllocationsEnabled) {
1514 ChangeNotifier.maybeDispatchObjectCreation(this);
1515 }
1516 }
1517
1518 static bool _equals(Uri a, Uri b) {
1519 return a.path == b.path &&
1520 a.fragment == b.fragment &&
1521 const DeepCollectionEquality.unordered().equals(a.queryParametersAll, b.queryParametersAll);
1522 }
1523
1524 @override
1525 void routerReportsNewRouteInformation(
1526 RouteInformation routeInformation, {
1527 RouteInformationReportingType type = RouteInformationReportingType.none,
1528 }) {
1529 SystemNavigator.selectMultiEntryHistory();
1530 SystemNavigator.routeInformationUpdated(
1531 uri: routeInformation.uri,
1532 state: routeInformation.state,
1533 replace: switch (type) {
1534 RouteInformationReportingType.neglect => true,
1535 RouteInformationReportingType.navigate => false,
1536 RouteInformationReportingType.none => _equals(_valueInEngine.uri, routeInformation.uri),
1537 },
1538 );
1539 _value = routeInformation;
1540 _valueInEngine = routeInformation;
1541 }
1542
1543 @override
1544 RouteInformation get value => _value;
1545 RouteInformation _value;
1546
1547 RouteInformation _valueInEngine = RouteInformation(
1548 uri: Uri.parse(WidgetsBinding.instance.platformDispatcher.defaultRouteName),
1549 );
1550
1551 void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
1552 if (_value == routeInformation) {
1553 return;
1554 }
1555 _value = routeInformation;
1556 _valueInEngine = routeInformation;
1557 notifyListeners();
1558 }
1559
1560 @override
1561 void addListener(VoidCallback listener) {
1562 if (!hasListeners) {
1563 WidgetsBinding.instance.addObserver(this);
1564 }
1565 super.addListener(listener);
1566 }
1567
1568 @override
1569 void removeListener(VoidCallback listener) {
1570 super.removeListener(listener);
1571 if (!hasListeners) {
1572 WidgetsBinding.instance.removeObserver(this);
1573 }
1574 }
1575
1576 @override
1577 void dispose() {
1578 // In practice, this will rarely be called. We assume that the listeners
1579 // will be added and removed in a coherent fashion such that when the object
1580 // is no longer being used, there's no listener, and so it will get garbage
1581 // collected.
1582 if (hasListeners) {
1583 WidgetsBinding.instance.removeObserver(this);
1584 }
1585 super.dispose();
1586 }
1587
1588 @override
1589 Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
1590 assert(hasListeners);
1591 _platformReportsNewRouteInformation(routeInformation);
1592 return true;
1593 }
1594}
1595
1596/// A mixin that wires [RouterDelegate.popRoute] to the [Navigator] it builds.
1597///
1598/// This mixin calls [Navigator.maybePop] when it receives an Android back
1599/// button intent through the [RouterDelegate.popRoute]. Using this mixin
1600/// guarantees that the back button still respects pageless routes in the
1601/// navigator.
1602///
1603/// Only use this mixin if you plan to build a navigator in the
1604/// [RouterDelegate.build].
1605mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
1606 /// The key used for retrieving the current navigator.
1607 ///
1608 /// When using this mixin, be sure to use this key to create the navigator.
1609 GlobalKey<NavigatorState>? get navigatorKey;
1610
1611 @override
1612 Future<bool> popRoute() {
1613 final NavigatorState? navigator = navigatorKey?.currentState;
1614 return navigator?.maybePop() ?? SynchronousFuture<bool>(false);
1615 }
1616}
1617
1618class _RestorableRouteInformation extends RestorableValue<RouteInformation?> {
1619 @override
1620 RouteInformation? createDefaultValue() => null;
1621
1622 @override
1623 void didUpdateValue(RouteInformation? oldValue) {
1624 notifyListeners();
1625 }
1626
1627 @override
1628 RouteInformation? fromPrimitives(Object? data) {
1629 if (data == null) {
1630 return null;
1631 }
1632 assert(data is List<Object?> && data.length == 2);
1633 final List<Object?> castedData = data as List<Object?>;
1634 final String? uri = castedData.first as String?;
1635 if (uri == null) {
1636 return null;
1637 }
1638 return RouteInformation(uri: Uri.parse(uri), state: castedData.last);
1639 }
1640
1641 @override
1642 Object? toPrimitives() {
1643 return value == null ? null : <Object?>[value!.uri.toString(), value!.state];
1644 }
1645}
1646

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com