| 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'; |
| 10 | library; |
| 11 | |
| 12 | import 'dart:async'; |
| 13 | import 'dart:collection'; |
| 14 | |
| 15 | import 'package:collection/collection.dart' ; |
| 16 | import 'package:flutter/foundation.dart'; |
| 17 | import 'package:flutter/scheduler.dart'; |
| 18 | import 'package:flutter/services.dart'; |
| 19 | |
| 20 | import 'basic.dart'; |
| 21 | import 'binding.dart'; |
| 22 | import 'framework.dart'; |
| 23 | import 'navigator.dart'; |
| 24 | import 'restoration.dart'; |
| 25 | import '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. |
| 49 | class 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. |
| 135 | class 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. |
| 358 | class 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 | |
| 580 | typedef _AsyncPassthrough<Q> = Future<Q> Function(Q); |
| 581 | typedef _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. |
| 590 | enum 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 | |
| 608 | class _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 | |
| 861 | class _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. |
| 900 | class _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: () => <DiagnosticsNode>[ |
| 944 | DiagnosticsProperty<_CallbackHookProvider<T>>( |
| 945 | 'The $runtimeType that invoked the callback was' , |
| 946 | this, |
| 947 | style: DiagnosticsTreeStyle.errorProperty, |
| 948 | ), |
| 949 | ], |
| 950 | ), |
| 951 | ); |
| 952 | return defaultValue; |
| 953 | } |
| 954 | } |
| 955 | } |
| 956 | |
| 957 | /// Report to a [Router] when the user taps the back button on platforms that |
| 958 | /// support back buttons (such as Android). |
| 959 | /// |
| 960 | /// When [Router] widgets are nested, consider using a |
| 961 | /// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher], |
| 962 | /// so that the back button requests get dispatched to the appropriate [Router]. |
| 963 | /// To make this work properly, it's important that whenever a [Router] thinks |
| 964 | /// it should get the back button messages (e.g. after the user taps inside it), |
| 965 | /// it calls [takePriority] on its [BackButtonDispatcher] (or |
| 966 | /// [ChildBackButtonDispatcher]) instance. |
| 967 | /// |
| 968 | /// The class takes a single callback, which must return a [Future<bool>]. The |
| 969 | /// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely, |
| 970 | /// the callback should return a future that completes to true if it can handle |
| 971 | /// the pop request, and a future that completes to false otherwise. |
| 972 | abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> { |
| 973 | late final LinkedHashSet<ChildBackButtonDispatcher> _children = |
| 974 | <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>; |
| 975 | |
| 976 | @override |
| 977 | bool get hasCallbacks => super.hasCallbacks || _children.isNotEmpty; |
| 978 | |
| 979 | /// Handles a pop route request. |
| 980 | /// |
| 981 | /// This method prioritizes the children list in reverse order and calls |
| 982 | /// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them |
| 983 | /// handles the request (by returning a future with true), it exits this |
| 984 | /// method by returning this future. Otherwise, it keeps moving on to the next |
| 985 | /// child until a child handles the request. If none of the children handles |
| 986 | /// the request, this back button dispatcher will then try to handle the request |
| 987 | /// by itself. This back button dispatcher handles the request by notifying the |
| 988 | /// router which in turn calls the [RouterDelegate.popRoute] and returns its |
| 989 | /// result. |
| 990 | /// |
| 991 | /// To decide whether this back button dispatcher will handle the pop route |
| 992 | /// request, you can override the [RouterDelegate.popRoute] of the router |
| 993 | /// delegate you pass into the router with this back button dispatcher to |
| 994 | /// return a future of true or false. |
| 995 | @override |
| 996 | Future<bool> invokeCallback(Future<bool> defaultValue) { |
| 997 | if (_children.isNotEmpty) { |
| 998 | final List<ChildBackButtonDispatcher> children = _children.toList(); |
| 999 | int childIndex = children.length - 1; |
| 1000 | |
| 1001 | Future<bool> notifyNextChild(bool result) { |
| 1002 | // If the previous child handles the callback, we return the result. |
| 1003 | if (result) { |
| 1004 | return SynchronousFuture<bool>(result); |
| 1005 | } |
| 1006 | // If the previous child did not handle the callback, we ask the next |
| 1007 | // child to handle the it. |
| 1008 | if (childIndex > 0) { |
| 1009 | childIndex -= 1; |
| 1010 | return children[childIndex].notifiedByParent(defaultValue).then<bool>(notifyNextChild); |
| 1011 | } |
| 1012 | // If none of the child handles the callback, the parent will then handle it. |
| 1013 | return super.invokeCallback(defaultValue); |
| 1014 | } |
| 1015 | |
| 1016 | return children[childIndex].notifiedByParent(defaultValue).then<bool>(notifyNextChild); |
| 1017 | } |
| 1018 | return super.invokeCallback(defaultValue); |
| 1019 | } |
| 1020 | |
| 1021 | /// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this |
| 1022 | /// back button dispatcher. |
| 1023 | /// |
| 1024 | /// To participate in handling the pop route request, call the [takePriority] |
| 1025 | /// on the [ChildBackButtonDispatcher] created from this method. |
| 1026 | /// |
| 1027 | /// When the pop route request is handled by this back button dispatcher, it |
| 1028 | /// propagate the request to its direct descendants that have called the |
| 1029 | /// [takePriority] method. If there are multiple candidates, the latest one |
| 1030 | /// that called the [takePriority] wins the right to handle the request. If |
| 1031 | /// the latest one does not handle the request (by returning a future of |
| 1032 | /// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest |
| 1033 | /// one will then have the right to handle the request. This dispatcher |
| 1034 | /// continues finding the next candidate until there are no more candidates |
| 1035 | /// and finally handles the request itself. |
| 1036 | ChildBackButtonDispatcher createChildBackButtonDispatcher() { |
| 1037 | return ChildBackButtonDispatcher(this); |
| 1038 | } |
| 1039 | |
| 1040 | /// Make this [BackButtonDispatcher] take priority among its peers. |
| 1041 | /// |
| 1042 | /// This has no effect when a [BackButtonDispatcher] has no parents and no |
| 1043 | /// children. If a [BackButtonDispatcher] does have parents or children, |
| 1044 | /// however, it causes this object to be the one to dispatch the notification |
| 1045 | /// when the parent would normally notify its callback. |
| 1046 | /// |
| 1047 | /// The [BackButtonDispatcher] must have a listener registered before it can |
| 1048 | /// be told to take priority. |
| 1049 | void takePriority() => _children.clear(); |
| 1050 | |
| 1051 | /// Mark the given child as taking priority over this object and the other |
| 1052 | /// children. |
| 1053 | /// |
| 1054 | /// This causes [invokeCallback] to defer to the given child instead of |
| 1055 | /// calling this object's callback. |
| 1056 | /// |
| 1057 | /// Children are stored in a list, so that if the current child is removed |
| 1058 | /// using [forget], a previous child will return to take its place. When |
| 1059 | /// [takePriority] is called, the list is cleared. |
| 1060 | /// |
| 1061 | /// Calling this again without first calling [forget] moves the child back to |
| 1062 | /// the head of the list. |
| 1063 | /// |
| 1064 | /// The [BackButtonDispatcher] must have a listener registered before it can |
| 1065 | /// be told to defer to a child. |
| 1066 | void deferTo(ChildBackButtonDispatcher child) { |
| 1067 | assert(hasCallbacks); |
| 1068 | _children.remove(child); // child may or may not be in the set already |
| 1069 | _children.add(child); |
| 1070 | } |
| 1071 | |
| 1072 | /// Causes the given child to be removed from the list of children to which |
| 1073 | /// this object might defer, as if [deferTo] had never been called for that |
| 1074 | /// child. |
| 1075 | /// |
| 1076 | /// This should only be called once per child, even if [deferTo] was called |
| 1077 | /// multiple times for that child. |
| 1078 | /// |
| 1079 | /// If no children are left in the list, this object will stop deferring to |
| 1080 | /// its children. (This is not the same as calling [takePriority], since, if |
| 1081 | /// this object itself is a [ChildBackButtonDispatcher], [takePriority] would |
| 1082 | /// additionally attempt to claim priority from its parent, whereas removing |
| 1083 | /// the last child does not.) |
| 1084 | void forget(ChildBackButtonDispatcher child) => _children.remove(child); |
| 1085 | } |
| 1086 | |
| 1087 | /// The default implementation of back button dispatcher for the root router. |
| 1088 | /// |
| 1089 | /// This dispatcher listens to platform pop route notifications. When the |
| 1090 | /// platform wants to pop the current route, this dispatcher calls the |
| 1091 | /// [BackButtonDispatcher.invokeCallback] method to handle the request. |
| 1092 | class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingObserver { |
| 1093 | /// Create a root back button dispatcher. |
| 1094 | RootBackButtonDispatcher(); |
| 1095 | |
| 1096 | @override |
| 1097 | void addCallback(ValueGetter<Future<bool>> callback) { |
| 1098 | if (!hasCallbacks) { |
| 1099 | WidgetsBinding.instance.addObserver(this); |
| 1100 | } |
| 1101 | super.addCallback(callback); |
| 1102 | } |
| 1103 | |
| 1104 | @override |
| 1105 | void removeCallback(ValueGetter<Future<bool>> callback) { |
| 1106 | super.removeCallback(callback); |
| 1107 | if (!hasCallbacks) { |
| 1108 | WidgetsBinding.instance.removeObserver(this); |
| 1109 | } |
| 1110 | } |
| 1111 | |
| 1112 | @override |
| 1113 | Future<bool> didPopRoute() => invokeCallback(Future<bool>.value(false)); |
| 1114 | } |
| 1115 | |
| 1116 | /// A variant of [BackButtonDispatcher] which listens to notifications from a |
| 1117 | /// parent back button dispatcher, and can take priority from its parent for the |
| 1118 | /// handling of such notifications. |
| 1119 | /// |
| 1120 | /// Useful when [Router]s are being nested within each other. |
| 1121 | /// |
| 1122 | /// Use [Router.of] to obtain a reference to the nearest ancestor [Router], from |
| 1123 | /// which the [Router.backButtonDispatcher] can be found, and then used as the |
| 1124 | /// [parent] of the [ChildBackButtonDispatcher]. |
| 1125 | class ChildBackButtonDispatcher extends BackButtonDispatcher { |
| 1126 | /// Creates a back button dispatcher that acts as the child of another. |
| 1127 | ChildBackButtonDispatcher(this.parent); |
| 1128 | |
| 1129 | /// The back button dispatcher that this object will attempt to take priority |
| 1130 | /// over when [takePriority] is called. |
| 1131 | /// |
| 1132 | /// The parent must have a listener registered before this child object can |
| 1133 | /// have its [takePriority] or [deferTo] methods used. |
| 1134 | final BackButtonDispatcher parent; |
| 1135 | |
| 1136 | /// The parent of this child back button dispatcher decide to let this |
| 1137 | /// child to handle the invoke the callback request in |
| 1138 | /// [BackButtonDispatcher.invokeCallback]. |
| 1139 | /// |
| 1140 | /// Return a boolean future with true if this child will handle the request; |
| 1141 | /// otherwise, return a boolean future with false. |
| 1142 | @protected |
| 1143 | Future<bool> notifiedByParent(Future<bool> defaultValue) { |
| 1144 | return invokeCallback(defaultValue); |
| 1145 | } |
| 1146 | |
| 1147 | @override |
| 1148 | void takePriority() { |
| 1149 | parent.deferTo(this); |
| 1150 | super.takePriority(); |
| 1151 | } |
| 1152 | |
| 1153 | @override |
| 1154 | void deferTo(ChildBackButtonDispatcher child) { |
| 1155 | assert(hasCallbacks); |
| 1156 | parent.deferTo(this); |
| 1157 | super.deferTo(child); |
| 1158 | } |
| 1159 | |
| 1160 | @override |
| 1161 | void removeCallback(ValueGetter<Future<bool>> callback) { |
| 1162 | super.removeCallback(callback); |
| 1163 | if (!hasCallbacks) { |
| 1164 | parent.forget(this); |
| 1165 | } |
| 1166 | } |
| 1167 | } |
| 1168 | |
| 1169 | /// A convenience widget that registers a callback for when the back button is pressed. |
| 1170 | /// |
| 1171 | /// In order to use this widget, there must be an ancestor [Router] widget in the tree |
| 1172 | /// that has a [RootBackButtonDispatcher]. e.g. The [Router] widget created by the |
| 1173 | /// [MaterialApp.router] has a built-in [RootBackButtonDispatcher] by default. |
| 1174 | /// |
| 1175 | /// It only applies to platforms that accept back button clicks, such as Android. |
| 1176 | /// |
| 1177 | /// It can be useful for scenarios, in which you create a different state in your |
| 1178 | /// screen but don't want to use a new page for that. |
| 1179 | class BackButtonListener extends StatefulWidget { |
| 1180 | /// Creates a BackButtonListener widget . |
| 1181 | const BackButtonListener({super.key, required this.child, required this.onBackButtonPressed}); |
| 1182 | |
| 1183 | /// The widget below this widget in the tree. |
| 1184 | final Widget child; |
| 1185 | |
| 1186 | /// The callback function that will be called when the back button is pressed. |
| 1187 | /// |
| 1188 | /// It must return a boolean future with true if this child will handle the request; |
| 1189 | /// otherwise, return a boolean future with false. |
| 1190 | final ValueGetter<Future<bool>> onBackButtonPressed; |
| 1191 | |
| 1192 | @override |
| 1193 | State<BackButtonListener> createState() => _BackButtonListenerState(); |
| 1194 | } |
| 1195 | |
| 1196 | class _BackButtonListenerState extends State<BackButtonListener> { |
| 1197 | BackButtonDispatcher? dispatcher; |
| 1198 | |
| 1199 | @override |
| 1200 | void didChangeDependencies() { |
| 1201 | dispatcher?.removeCallback(widget.onBackButtonPressed); |
| 1202 | |
| 1203 | final BackButtonDispatcher? rootBackDispatcher = Router.of(context).backButtonDispatcher; |
| 1204 | assert( |
| 1205 | rootBackDispatcher != null, |
| 1206 | 'The parent router must have a backButtonDispatcher to use this widget' , |
| 1207 | ); |
| 1208 | |
| 1209 | dispatcher = rootBackDispatcher!.createChildBackButtonDispatcher() |
| 1210 | ..addCallback(widget.onBackButtonPressed) |
| 1211 | ..takePriority(); |
| 1212 | super.didChangeDependencies(); |
| 1213 | } |
| 1214 | |
| 1215 | @override |
| 1216 | void didUpdateWidget(covariant BackButtonListener oldWidget) { |
| 1217 | super.didUpdateWidget(oldWidget); |
| 1218 | if (oldWidget.onBackButtonPressed != widget.onBackButtonPressed) { |
| 1219 | dispatcher?.removeCallback(oldWidget.onBackButtonPressed); |
| 1220 | dispatcher?.addCallback(widget.onBackButtonPressed); |
| 1221 | dispatcher?.takePriority(); |
| 1222 | } |
| 1223 | } |
| 1224 | |
| 1225 | @override |
| 1226 | void dispose() { |
| 1227 | dispatcher?.removeCallback(widget.onBackButtonPressed); |
| 1228 | super.dispose(); |
| 1229 | } |
| 1230 | |
| 1231 | @override |
| 1232 | Widget build(BuildContext context) => widget.child; |
| 1233 | } |
| 1234 | |
| 1235 | /// A delegate that is used by the [Router] widget to parse a route information |
| 1236 | /// into a configuration of type T. |
| 1237 | /// |
| 1238 | /// This delegate is used when the [Router] widget is first built with initial |
| 1239 | /// route information from [Router.routeInformationProvider] and any subsequent |
| 1240 | /// new route notifications from it. The [Router] widget calls the [parseRouteInformation] |
| 1241 | /// with the route information from [Router.routeInformationProvider]. |
| 1242 | /// |
| 1243 | /// One of the [parseRouteInformation] or |
| 1244 | /// [parseRouteInformationWithDependencies] must be implemented, otherwise a |
| 1245 | /// runtime error will be thrown. |
| 1246 | abstract class RouteInformationParser<T> { |
| 1247 | /// Abstract const constructor. This constructor enables subclasses to provide |
| 1248 | /// const constructors so that they can be used in const expressions. |
| 1249 | const RouteInformationParser(); |
| 1250 | |
| 1251 | /// {@template flutter.widgets.RouteInformationParser.parseRouteInformation} |
| 1252 | /// Converts the given route information into parsed data to pass to a |
| 1253 | /// [RouterDelegate]. |
| 1254 | /// |
| 1255 | /// The method should return a future which completes when the parsing is |
| 1256 | /// complete. The parsing may be asynchronous if, e.g., the parser needs to |
| 1257 | /// communicate with the OEM thread to obtain additional data about the route. |
| 1258 | /// |
| 1259 | /// Consider using a [SynchronousFuture] if the result can be computed |
| 1260 | /// synchronously, so that the [Router] does not need to wait for the next |
| 1261 | /// microtask to pass the data to the [RouterDelegate]. |
| 1262 | /// {@endtemplate} |
| 1263 | /// |
| 1264 | /// One can implement [parseRouteInformationWithDependencies] instead if |
| 1265 | /// the parsing depends on other dependencies from the [BuildContext]. |
| 1266 | Future<T> parseRouteInformation(RouteInformation routeInformation) { |
| 1267 | throw UnimplementedError( |
| 1268 | 'One of the parseRouteInformation or ' |
| 1269 | 'parseRouteInformationWithDependencies must be implemented' , |
| 1270 | ); |
| 1271 | } |
| 1272 | |
| 1273 | /// {@macro flutter.widgets.RouteInformationParser.parseRouteInformation} |
| 1274 | /// |
| 1275 | /// The input [BuildContext] can be used for looking up [InheritedWidget]s |
| 1276 | /// If one uses [BuildContext.dependOnInheritedWidgetOfExactType], a |
| 1277 | /// dependency will be created. The [Router] will re-parse the |
| 1278 | /// [RouteInformation] from its [RouteInformationProvider] if the dependency |
| 1279 | /// notifies its listeners. |
| 1280 | /// |
| 1281 | /// One can also use [BuildContext.getElementForInheritedWidgetOfExactType] to |
| 1282 | /// look up [InheritedWidget]s without creating dependencies. |
| 1283 | Future<T> parseRouteInformationWithDependencies( |
| 1284 | RouteInformation routeInformation, |
| 1285 | BuildContext context, |
| 1286 | ) { |
| 1287 | return parseRouteInformation(routeInformation); |
| 1288 | } |
| 1289 | |
| 1290 | /// Restore the route information from the given configuration. |
| 1291 | /// |
| 1292 | /// This may return null, in which case the browser history will not be |
| 1293 | /// updated and state restoration is disabled. See [Router]'s documentation |
| 1294 | /// for details. |
| 1295 | /// |
| 1296 | /// The [parseRouteInformation] method must produce an equivalent |
| 1297 | /// configuration when passed this method's return value. |
| 1298 | RouteInformation? restoreRouteInformation(T configuration) => null; |
| 1299 | } |
| 1300 | |
| 1301 | /// A delegate that is used by the [Router] widget to build and configure a |
| 1302 | /// navigating widget. |
| 1303 | /// |
| 1304 | /// This delegate is the core piece of the [Router] widget. It responds to |
| 1305 | /// push route and pop route intents from the engine and notifies the [Router] |
| 1306 | /// to rebuild. It also acts as a builder for the [Router] widget and builds a |
| 1307 | /// navigating widget, typically a [Navigator], when the [Router] widget |
| 1308 | /// builds. |
| 1309 | /// |
| 1310 | /// When the engine pushes a new route, the route information is parsed by the |
| 1311 | /// [RouteInformationParser] to produce a configuration of type T. The router |
| 1312 | /// delegate receives the configuration through [setInitialRoutePath] or |
| 1313 | /// [setNewRoutePath] to configure itself and builds the latest navigating |
| 1314 | /// widget when asked ([build]). |
| 1315 | /// |
| 1316 | /// When implementing subclasses, consider defining a [Listenable] app state object to be |
| 1317 | /// used for building the navigating widget. The router delegate would update |
| 1318 | /// the app state accordingly and notify its own listeners when the app state has |
| 1319 | /// changed and when it receive route related engine intents (e.g. |
| 1320 | /// [setNewRoutePath], [setInitialRoutePath], or [popRoute]). |
| 1321 | /// |
| 1322 | /// All subclass must implement [setNewRoutePath], [popRoute], and [build]. |
| 1323 | /// |
| 1324 | /// ## State Restoration |
| 1325 | /// |
| 1326 | /// If the [Router] owning this delegate is configured for state restoration, it |
| 1327 | /// will persist and restore the configuration of this [RouterDelegate] using |
| 1328 | /// the following mechanism: Before the app is killed by the operating system, |
| 1329 | /// the value of [currentConfiguration] is serialized out and persisted. After |
| 1330 | /// the app has restarted, the value is deserialized and passed back to the |
| 1331 | /// [RouterDelegate] via a call to [setRestoredRoutePath] (which by default just |
| 1332 | /// calls [setNewRoutePath]). It is the responsibility of the [RouterDelegate] |
| 1333 | /// to use the configuration information provided to restore its internal state. |
| 1334 | /// |
| 1335 | /// See also: |
| 1336 | /// |
| 1337 | /// * [RouteInformationParser], which is responsible for parsing the route |
| 1338 | /// information to a configuration before passing in to router delegate. |
| 1339 | /// * [Router], which is the widget that wires all the delegates together to |
| 1340 | /// provide a fully functional routing solution. |
| 1341 | abstract class RouterDelegate<T> extends Listenable { |
| 1342 | /// Called by the [Router] at startup with the structure that the |
| 1343 | /// [RouteInformationParser] obtained from parsing the initial route. |
| 1344 | /// |
| 1345 | /// This should configure the [RouterDelegate] so that when [build] is |
| 1346 | /// invoked, it will create a widget tree that matches the initial route. |
| 1347 | /// |
| 1348 | /// By default, this method forwards the [configuration] to [setNewRoutePath]. |
| 1349 | /// |
| 1350 | /// Consider using a [SynchronousFuture] if the result can be computed |
| 1351 | /// synchronously, so that the [Router] does not need to wait for the next |
| 1352 | /// microtask to schedule a build. |
| 1353 | /// |
| 1354 | /// See also: |
| 1355 | /// |
| 1356 | /// * [setRestoredRoutePath], which is called instead of this method during |
| 1357 | /// state restoration. |
| 1358 | Future<void> setInitialRoutePath(T configuration) { |
| 1359 | return setNewRoutePath(configuration); |
| 1360 | } |
| 1361 | |
| 1362 | /// Called by the [Router] during state restoration. |
| 1363 | /// |
| 1364 | /// When the [Router] is configured for state restoration, it will persist |
| 1365 | /// the value of [currentConfiguration] during state serialization. During |
| 1366 | /// state restoration, the [Router] calls this method (instead of |
| 1367 | /// [setInitialRoutePath]) to pass the previous configuration back to the |
| 1368 | /// delegate. It is the responsibility of the delegate to restore its internal |
| 1369 | /// state based on the provided configuration. |
| 1370 | /// |
| 1371 | /// By default, this method forwards the `configuration` to [setNewRoutePath]. |
| 1372 | Future<void> setRestoredRoutePath(T configuration) { |
| 1373 | return setNewRoutePath(configuration); |
| 1374 | } |
| 1375 | |
| 1376 | /// Called by the [Router] when the [Router.routeInformationProvider] reports that a |
| 1377 | /// new route has been pushed to the application by the operating system. |
| 1378 | /// |
| 1379 | /// Consider using a [SynchronousFuture] if the result can be computed |
| 1380 | /// synchronously, so that the [Router] does not need to wait for the next |
| 1381 | /// microtask to schedule a build. |
| 1382 | Future<void> setNewRoutePath(T configuration); |
| 1383 | |
| 1384 | /// Called by the [Router] when the [Router.backButtonDispatcher] reports that |
| 1385 | /// the operating system is requesting that the current route be popped. |
| 1386 | /// |
| 1387 | /// The method should return a boolean [Future] to indicate whether this |
| 1388 | /// delegate handles the request. Returning false will cause the entire app |
| 1389 | /// to be popped. |
| 1390 | /// |
| 1391 | /// Consider using a [SynchronousFuture] if the result can be computed |
| 1392 | /// synchronously, so that the [Router] does not need to wait for the next |
| 1393 | /// microtask to schedule a build. |
| 1394 | Future<bool> popRoute(); |
| 1395 | |
| 1396 | /// Called by the [Router] when it detects a route information may have |
| 1397 | /// changed as a result of rebuild. |
| 1398 | /// |
| 1399 | /// If this getter returns non-null, the [Router] will start to report new |
| 1400 | /// route information back to the engine. In web applications, the new |
| 1401 | /// route information is used for populating browser history in order to |
| 1402 | /// support the forward and the backward buttons. |
| 1403 | /// |
| 1404 | /// When overriding this method, the configuration returned by this getter |
| 1405 | /// must be able to construct the current app state and build the widget |
| 1406 | /// with the same configuration in the [build] method if it is passed back |
| 1407 | /// to the [setNewRoutePath]. Otherwise, the browser backward and forward |
| 1408 | /// buttons will not work properly. |
| 1409 | /// |
| 1410 | /// By default, this getter returns null, which prevents the [Router] from |
| 1411 | /// reporting the route information. To opt in, a subclass can override this |
| 1412 | /// getter to return the current configuration. |
| 1413 | /// |
| 1414 | /// At most one [Router] can opt in to route information reporting. Typically, |
| 1415 | /// only the top-most [Router] created by [WidgetsApp.router] should opt for |
| 1416 | /// route information reporting. |
| 1417 | /// |
| 1418 | /// ## State Restoration |
| 1419 | /// |
| 1420 | /// This getter is also used by the [Router] to implement state restoration. |
| 1421 | /// During state serialization, the [Router] will persist the current |
| 1422 | /// configuration and during state restoration pass it back to the delegate |
| 1423 | /// by calling [setRestoredRoutePath]. |
| 1424 | T? get currentConfiguration => null; |
| 1425 | |
| 1426 | /// Called by the [Router] to obtain the widget tree that represents the |
| 1427 | /// current state. |
| 1428 | /// |
| 1429 | /// This is called whenever the [Future]s returned by [setInitialRoutePath], |
| 1430 | /// [setNewRoutePath], or [setRestoredRoutePath] complete as well as when this |
| 1431 | /// notifies its clients (see the [Listenable] interface, which this interface |
| 1432 | /// includes). In addition, it may be called at other times. It is important, |
| 1433 | /// therefore, that the methods above do not update the state that the [build] |
| 1434 | /// method uses before they complete their respective futures. |
| 1435 | /// |
| 1436 | /// Typically this method returns a suitably-configured [Navigator]. If you do |
| 1437 | /// plan to create a navigator, consider using the |
| 1438 | /// [PopNavigatorRouterDelegateMixin]. If state restoration is enabled for the |
| 1439 | /// [Router] using this delegate, consider providing a non-null |
| 1440 | /// [Navigator.restorationScopeId] to the [Navigator] returned by this method. |
| 1441 | /// |
| 1442 | /// This method must not return null. |
| 1443 | /// |
| 1444 | /// The `context` is the [Router]'s build context. |
| 1445 | Widget build(BuildContext context); |
| 1446 | } |
| 1447 | |
| 1448 | /// A route information provider that provides route information for the |
| 1449 | /// [Router] widget |
| 1450 | /// |
| 1451 | /// This provider is responsible for handing the route information through [value] |
| 1452 | /// getter and notifies listeners, typically the [Router] widget, when a new |
| 1453 | /// route information is available. |
| 1454 | /// |
| 1455 | /// When the router opts for route information reporting (by overriding the |
| 1456 | /// [RouterDelegate.currentConfiguration] to return non-null), override the |
| 1457 | /// [routerReportsNewRouteInformation] method to process the route information. |
| 1458 | /// |
| 1459 | /// See also: |
| 1460 | /// |
| 1461 | /// * [PlatformRouteInformationProvider], which wires up the itself with the |
| 1462 | /// [WidgetsBindingObserver.didPushRoute] to propagate platform push route |
| 1463 | /// intent to the [Router] widget, as well as reports new route information |
| 1464 | /// from the [Router] back to the engine by overriding the |
| 1465 | /// [routerReportsNewRouteInformation]. |
| 1466 | abstract class RouteInformationProvider extends ValueListenable<RouteInformation> { |
| 1467 | /// A callback called when the [Router] widget reports new route information |
| 1468 | /// |
| 1469 | /// The subclasses can override this method to update theirs values or trigger |
| 1470 | /// other side effects. For example, the [PlatformRouteInformationProvider] |
| 1471 | /// overrides this method to report the route information back to the engine. |
| 1472 | /// |
| 1473 | /// The `routeInformation` is the new route information generated by the |
| 1474 | /// Router rebuild, and it can be the same or different from the |
| 1475 | /// [value]. |
| 1476 | /// |
| 1477 | /// The `type` denotes the [Router]'s intention when it reports this |
| 1478 | /// `routeInformation`. It is useful when deciding how to update the internal |
| 1479 | /// state of [RouteInformationProvider] subclass with the `routeInformation`. |
| 1480 | /// For example, [PlatformRouteInformationProvider] uses this property to |
| 1481 | /// decide whether to push or replace the browser history entry with the new |
| 1482 | /// `routeInformation`. |
| 1483 | /// |
| 1484 | /// For more information on how [Router] determines a navigation event, see |
| 1485 | /// the "URL updates for web applications" section in the [Router] |
| 1486 | /// documentation. |
| 1487 | void routerReportsNewRouteInformation( |
| 1488 | RouteInformation routeInformation, { |
| 1489 | RouteInformationReportingType type = RouteInformationReportingType.none, |
| 1490 | }) {} |
| 1491 | } |
| 1492 | |
| 1493 | /// The route information provider that propagates the platform route information changes. |
| 1494 | /// |
| 1495 | /// This provider also reports the new route information from the [Router] widget |
| 1496 | /// back to engine using message channel method, the |
| 1497 | /// [SystemNavigator.routeInformationUpdated]. |
| 1498 | /// |
| 1499 | /// Each time [SystemNavigator.routeInformationUpdated] is called, the |
| 1500 | /// [SystemNavigator.selectMultiEntryHistory] method is also called. This |
| 1501 | /// overrides the initialization behavior of |
| 1502 | /// [Navigator.reportsRouteUpdateToEngine]. |
| 1503 | class PlatformRouteInformationProvider extends RouteInformationProvider |
| 1504 | with WidgetsBindingObserver, ChangeNotifier { |
| 1505 | /// Create a platform route information provider. |
| 1506 | /// |
| 1507 | /// Use the [initialRouteInformation] to set the default route information for this |
| 1508 | /// provider. |
| 1509 | PlatformRouteInformationProvider({required RouteInformation initialRouteInformation}) |
| 1510 | : _value = initialRouteInformation { |
| 1511 | if (kFlutterMemoryAllocationsEnabled) { |
| 1512 | ChangeNotifier.maybeDispatchObjectCreation(this); |
| 1513 | } |
| 1514 | } |
| 1515 | |
| 1516 | static bool _equals(Uri a, Uri b) { |
| 1517 | return a.path == b.path && |
| 1518 | a.fragment == b.fragment && |
| 1519 | const DeepCollectionEquality.unordered().equals(a.queryParametersAll, b.queryParametersAll); |
| 1520 | } |
| 1521 | |
| 1522 | @override |
| 1523 | void routerReportsNewRouteInformation( |
| 1524 | RouteInformation routeInformation, { |
| 1525 | RouteInformationReportingType type = RouteInformationReportingType.none, |
| 1526 | }) { |
| 1527 | SystemNavigator.selectMultiEntryHistory(); |
| 1528 | SystemNavigator.routeInformationUpdated( |
| 1529 | uri: routeInformation.uri, |
| 1530 | state: routeInformation.state, |
| 1531 | replace: switch (type) { |
| 1532 | RouteInformationReportingType.neglect => true, |
| 1533 | RouteInformationReportingType.navigate => false, |
| 1534 | RouteInformationReportingType.none => _equals(_valueInEngine.uri, routeInformation.uri), |
| 1535 | }, |
| 1536 | ); |
| 1537 | _value = routeInformation; |
| 1538 | _valueInEngine = routeInformation; |
| 1539 | } |
| 1540 | |
| 1541 | @override |
| 1542 | RouteInformation get value => _value; |
| 1543 | RouteInformation _value; |
| 1544 | |
| 1545 | RouteInformation _valueInEngine = RouteInformation( |
| 1546 | uri: Uri.parse(WidgetsBinding.instance.platformDispatcher.defaultRouteName), |
| 1547 | ); |
| 1548 | |
| 1549 | void _platformReportsNewRouteInformation(RouteInformation routeInformation) { |
| 1550 | if (_value == routeInformation) { |
| 1551 | return; |
| 1552 | } |
| 1553 | _value = routeInformation; |
| 1554 | _valueInEngine = routeInformation; |
| 1555 | notifyListeners(); |
| 1556 | } |
| 1557 | |
| 1558 | @override |
| 1559 | void addListener(VoidCallback listener) { |
| 1560 | if (!hasListeners) { |
| 1561 | WidgetsBinding.instance.addObserver(this); |
| 1562 | } |
| 1563 | super.addListener(listener); |
| 1564 | } |
| 1565 | |
| 1566 | @override |
| 1567 | void removeListener(VoidCallback listener) { |
| 1568 | super.removeListener(listener); |
| 1569 | if (!hasListeners) { |
| 1570 | WidgetsBinding.instance.removeObserver(this); |
| 1571 | } |
| 1572 | } |
| 1573 | |
| 1574 | @override |
| 1575 | void dispose() { |
| 1576 | // In practice, this will rarely be called. We assume that the listeners |
| 1577 | // will be added and removed in a coherent fashion such that when the object |
| 1578 | // is no longer being used, there's no listener, and so it will get garbage |
| 1579 | // collected. |
| 1580 | if (hasListeners) { |
| 1581 | WidgetsBinding.instance.removeObserver(this); |
| 1582 | } |
| 1583 | super.dispose(); |
| 1584 | } |
| 1585 | |
| 1586 | @override |
| 1587 | Future<bool> didPushRouteInformation(RouteInformation routeInformation) async { |
| 1588 | assert(hasListeners); |
| 1589 | _platformReportsNewRouteInformation(routeInformation); |
| 1590 | return true; |
| 1591 | } |
| 1592 | } |
| 1593 | |
| 1594 | /// A mixin that wires [RouterDelegate.popRoute] to the [Navigator] it builds. |
| 1595 | /// |
| 1596 | /// This mixin calls [Navigator.maybePop] when it receives an Android back |
| 1597 | /// button intent through the [RouterDelegate.popRoute]. Using this mixin |
| 1598 | /// guarantees that the back button still respects pageless routes in the |
| 1599 | /// navigator. |
| 1600 | /// |
| 1601 | /// Only use this mixin if you plan to build a navigator in the |
| 1602 | /// [RouterDelegate.build]. |
| 1603 | mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> { |
| 1604 | /// The key used for retrieving the current navigator. |
| 1605 | /// |
| 1606 | /// When using this mixin, be sure to use this key to create the navigator. |
| 1607 | GlobalKey<NavigatorState>? get navigatorKey; |
| 1608 | |
| 1609 | @override |
| 1610 | Future<bool> popRoute() { |
| 1611 | final NavigatorState? navigator = navigatorKey?.currentState; |
| 1612 | return navigator?.maybePop() ?? SynchronousFuture<bool>(false); |
| 1613 | } |
| 1614 | } |
| 1615 | |
| 1616 | class _RestorableRouteInformation extends RestorableValue<RouteInformation?> { |
| 1617 | @override |
| 1618 | RouteInformation? createDefaultValue() => null; |
| 1619 | |
| 1620 | @override |
| 1621 | void didUpdateValue(RouteInformation? oldValue) { |
| 1622 | notifyListeners(); |
| 1623 | } |
| 1624 | |
| 1625 | @override |
| 1626 | RouteInformation? fromPrimitives(Object? data) { |
| 1627 | if (data == null) { |
| 1628 | return null; |
| 1629 | } |
| 1630 | assert(data is List<Object?> && data.length == 2); |
| 1631 | final List<Object?> castedData = data as List<Object?>; |
| 1632 | final String? uri = castedData.first as String?; |
| 1633 | if (uri == null) { |
| 1634 | return null; |
| 1635 | } |
| 1636 | return RouteInformation(uri: Uri.parse(uri), state: castedData.last); |
| 1637 | } |
| 1638 | |
| 1639 | @override |
| 1640 | Object? toPrimitives() { |
| 1641 | return value == null ? null : <Object?>[value!.uri.toString(), value!.state]; |
| 1642 | } |
| 1643 | } |
| 1644 | |