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