/
router.dart
1593 lines (1479 loc) · 67.1 KB
/
router.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
///
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case happens in web application where the [Router] reports route
/// changes back to the web engine.
///
/// The current [RouteInformation] of an application is also used for state
/// restoration purposes. Before an application is killed, the [Router] converts
/// its current configurations into a [RouteInformation] object utilizing the
/// [RouteInformationProvider]. The [RouteInformation] object is then serialized
/// out and persisted. During state restoration, the object is deserialized and
/// passed back to the [RouteInformationProvider], which turns it into a
/// configuration for the [Router] again to restore its state from.
class RouteInformation {
/// Creates a route information object.
///
/// Either `location` or `uri` must not be null.
const RouteInformation({
@Deprecated(
'Pass Uri.parse(location) to uri parameter instead. '
'This feature was deprecated after v3.8.0-3.0.pre.'
)
String? location,
Uri? uri,
this.state,
}) : _location = location,
_uri = uri,
assert((location != null) != (uri != null));
/// The location of the application.
///
/// The string is usually in the format of multiple string identifiers with
/// slashes in between. ex: `/`, `/path`, `/path/to/the/app`.
@Deprecated(
'Use uri instead. '
'This feature was deprecated after v3.8.0-3.0.pre.'
)
String get location {
return _location ?? Uri.decodeComponent(
Uri(
path: uri.path.isEmpty ? '/' : uri.path,
queryParameters: uri.queryParametersAll.isEmpty ? null : uri.queryParametersAll,
fragment: uri.fragment.isEmpty ? null : uri.fragment,
).toString(),
);
}
final String? _location;
/// The uri location of the application.
///
/// The host and scheme will not be empty if this object is created from a
/// deep link request. They represents the website that redirect the deep
/// link.
///
/// In web platform, the host and scheme are always empty.
Uri get uri {
if (_uri != null){
return _uri;
}
return Uri.parse(_location!);
}
final Uri? _uri;
/// The state of the application in the [uri].
///
/// The app can have different states even in the same location. For example,
/// the text inside a [TextField] or the scroll position in a [ScrollView].
/// These widget states can be stored in the [state].
///
/// On the web, this information is stored in the browser history when the
/// [Router] reports this route information back to the web engine
/// through the [PlatformRouteInformationProvider]. The information
/// is then passed back, along with the [uri], when the user
/// clicks the back or forward buttons.
///
/// This information is also serialized and persisted alongside the
/// [uri] for state restoration purposes. During state restoration,
/// the information is made available again to the [Router] so it can restore
/// its configuration to the previous state.
///
/// The state must be serializable.
final Object? state;
}
/// A convenient bundle to configure a [Router] widget.
///
/// To configure a [Router] widget, one needs to provide several delegates,
/// [RouteInformationProvider], [RouteInformationParser], [RouterDelegate],
/// and [BackButtonDispatcher]. This abstract class provides way to bundle these
/// delegates into a single object to configure a [Router].
///
/// The [backButtonDispatcher], [routeInformationProvider], and
/// [routeInformationProvider] are optional.
///
/// The [routeInformationProvider] and [routeInformationParser] must
/// both be provided or both not provided.
class RouterConfig<T> {
/// Creates a [RouterConfig].
///
/// The [backButtonDispatcher], [routeInformationProvider], and
/// [routeInformationParser] are optional.
///
/// The [routeInformationProvider] and [routeInformationParser] must both be
/// provided or both not provided.
const RouterConfig({
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
}) : assert((routeInformationProvider == null) == (routeInformationParser == null));
/// The [RouteInformationProvider] that is used to configure the [Router].
final RouteInformationProvider? routeInformationProvider;
/// The [RouteInformationParser] that is used to configure the [Router].
final RouteInformationParser<T>? routeInformationParser;
/// The [RouterDelegate] that is used to configure the [Router].
final RouterDelegate<T> routerDelegate;
/// The [BackButtonDispatcher] that is used to configure the [Router].
final BackButtonDispatcher? backButtonDispatcher;
}
/// The dispatcher for opening and closing pages of an application.
///
/// This widget listens for routing information from the operating system (e.g.
/// an initial route provided on app startup, a new route obtained when an
/// intent is received, or a notification that the user hit the system back
/// button), parses route information into data of type `T`, and then converts
/// that data into [Page] objects that it passes to a [Navigator].
///
/// Each part of this process can be overridden and configured as desired.
///
/// The [routeInformationProvider] can be overridden to change how the name of
/// the route is obtained. The [RouteInformationProvider.value] is used as the
/// initial route when the [Router] is first created. Subsequent notifications
/// from the [RouteInformationProvider] to its listeners are treated as
/// notifications that the route information has changed.
///
/// The [backButtonDispatcher] can be overridden to change how back button
/// notifications are received. This must be a [BackButtonDispatcher], which is
/// an object where callbacks can be registered, and which can be chained so
/// that back button presses are delegated to subsidiary routers. The callbacks
/// are invoked to indicate that the user is trying to close the current route
/// (by pressing the system back button); the [Router] ensures that when this
/// callback is invoked, the message is passed to the [routerDelegate] and its
/// result is provided back to the [backButtonDispatcher]. Some platforms don't
/// have back buttons (e.g. iOS and desktop platforms); on those platforms this
/// notification is never sent. Typically, the [backButtonDispatcher] for the
/// root router is an instance of [RootBackButtonDispatcher], which uses a
/// [WidgetsBindingObserver] to listen to the `popRoute` notifications from
/// [SystemChannels.navigation]. Nested [Router]s typically use a
/// [ChildBackButtonDispatcher], which must be provided the
/// [BackButtonDispatcher] of its ancestor [Router] (available via [Router.of]).
///
/// The [routeInformationParser] can be overridden to change how names obtained
/// from the [routeInformationProvider] are interpreted. It must implement the
/// [RouteInformationParser] interface, specialized with the same type as the
/// [Router] itself. This type, `T`, represents the data type that the
/// [routeInformationParser] will generate.
///
/// The [routerDelegate] can be overridden to change how the output of the
/// [routeInformationParser] is interpreted. It must implement the
/// [RouterDelegate] interface, also specialized with `T`; it takes as input
/// the data (of type `T`) from the [routeInformationParser], and is responsible
/// for providing a navigating widget to insert into the widget tree. The
/// [RouterDelegate] interface is also [Listenable]; notifications are taken
/// to mean that the [Router] needs to rebuild.
///
/// ## Concerns regarding asynchrony
///
/// Some of the APIs (notably those involving [RouteInformationParser] and
/// [RouterDelegate]) are asynchronous.
///
/// When developing objects implementing these APIs, if the work can be done
/// entirely synchronously, then consider using [SynchronousFuture] for the
/// future returned from the relevant methods. This will allow the [Router] to
/// proceed in a completely synchronous way, which removes a number of
/// complications.
///
/// Using asynchronous computation is entirely reasonable, however, and the API
/// is designed to support it. For example, maybe a set of images need to be
/// loaded before a route can be shown; waiting for those images to be loaded
/// before [RouterDelegate.setNewRoutePath] returns is a reasonable approach to
/// handle this case.
///
/// If an asynchronous operation is ongoing when a new one is to be started, the
/// precise behavior will depend on the exact circumstances, as follows:
///
/// If the active operation is a [routeInformationParser] parsing a new route information:
/// that operation's result, if it ever completes, will be discarded.
///
/// If the active operation is a [routerDelegate] handling a pop request:
/// the previous pop is immediately completed with "false", claiming that the
/// previous pop was not handled (this may cause the application to close).
///
/// If the active operation is a [routerDelegate] handling an initial route
/// or a pushed route, the result depends on the new operation. If the new
/// operation is a pop request, then the original operation's result, if it ever
/// completes, will be discarded. If the new operation is a push request,
/// however, the [routeInformationParser] will be requested to start the parsing, and
/// only if that finishes before the original [routerDelegate] request
/// completes will that original request's result be discarded.
///
/// If the identity of the [Router] widget's delegates change while an
/// asynchronous operation is in progress, to keep matters simple, all active
/// asynchronous operations will have their results discarded. It is generally
/// considered unusual for these delegates to change during the lifetime of the
/// [Router].
///
/// If the [Router] itself is disposed while an asynchronous operation is in
/// progress, all active asynchronous operations will have their results
/// discarded also.
///
/// No explicit signals are provided to the [routeInformationParser] or
/// [routerDelegate] to indicate when any of the above happens, so it is
/// strongly recommended that [RouteInformationParser] and [RouterDelegate]
/// implementations not perform extensive computation.
///
/// ## Application architectural design
///
/// An application can have zero, one, or many [Router] widgets, depending on
/// its needs.
///
/// An application might have no [Router] widgets if it has only one "screen",
/// or if the facilities provided by [Navigator] are sufficient. This is common
/// for desktop applications, where subsidiary "screens" are represented using
/// different windows rather than changing the active interface.
///
/// A particularly elaborate application might have multiple [Router] widgets,
/// in a tree configuration, with the first handling the entire route parsing
/// and making the result available for routers in the subtree. The routers in
/// the subtree do not participate in route information parsing but merely take the
/// result from the first router to build their sub routes.
///
/// Most applications only need a single [Router].
///
/// ## URL updates for web applications
///
/// In the web platform, keeping the URL in the browser's location bar up to
/// date with the application state ensures that the browser constructs its
/// history entry correctly, allowing its back and forward buttons to function
/// as the user expects.
///
/// If an app state change leads to the [Router] rebuilding, the [Router] will
/// retrieve the new route information from the [routerDelegate]'s
/// [RouterDelegate.currentConfiguration] method and the
/// [routeInformationParser]'s [RouteInformationParser.restoreRouteInformation]
/// method.
///
/// If the location in the new route information is different from the
/// current location, this is considered to be a navigation event, the
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] method
/// calls [SystemNavigator.routeInformationUpdated] with `replace = false` to
/// notify the engine, and through that the browser, to create a history entry
/// with the new url. Otherwise,
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation] calls
/// [SystemNavigator.routeInformationUpdated] with `replace = true` to update
/// the current history entry with the latest [RouteInformation].
///
/// One can force the [Router] to report new route information as navigation
/// event to the [routeInformationProvider] (and thus the browser) even if the
/// [RouteInformation.uri] has not changed by calling the [Router.navigate]
/// method with a callback that performs the state change. This causes [Router]
/// to call the [RouteInformationProvider.routerReportsNewRouteInformation] with
/// [RouteInformationReportingType.navigate], and thus causes
/// [PlatformRouteInformationProvider] to push a new history entry regardlessly.
/// This allows one to support the browser's back and forward buttons without
/// changing the URL. For example, the scroll position of a scroll view may be
/// saved in the [RouteInformation.state]. Using [Router.navigate] to update the
/// scroll position causes the browser to create a new history entry with the
/// [RouteInformation.state] that stores this new scroll position. When the user
/// clicks the back button, the app will go back to the previous scroll position
/// without changing the URL in the location bar.
///
/// One can also force the [Router] to ignore a navigation event by making
/// those changes during a callback passed to [Router.neglect]. The [Router]
/// calls the [RouteInformationProvider.routerReportsNewRouteInformation] with
/// [RouteInformationReportingType.neglect], and thus causes
/// [PlatformRouteInformationProvider] to replace the current history entry
/// regardlessly even if it detects location change.
///
/// To opt out of URL updates entirely, pass null for [routeInformationProvider]
/// and [routeInformationParser]. This is not recommended in general, but may be
/// appropriate in the following cases:
///
/// * The application does not target the web platform.
///
/// * There are multiple router widgets in the application. Only one [Router]
/// widget should update the URL (typically the top-most one created by the
/// [WidgetsApp.router], [MaterialApp.router], or [CupertinoApp.router]).
///
/// * The application does not need to implement in-app navigation using the
/// browser's back and forward buttons.
///
/// In other cases, it is strongly recommended to implement the
/// [RouterDelegate.currentConfiguration] and
/// [RouteInformationParser.restoreRouteInformation] APIs to provide an optimal
/// user experience when running on the web platform.
///
/// ## State Restoration
///
/// The [Router] will restore the current configuration of the [routerDelegate]
/// during state restoration if it is configured with a [restorationScopeId] and
/// state restoration is enabled for the subtree. For that, the value of
/// [RouterDelegate.currentConfiguration] is serialized and persisted before the
/// app is killed by the operating system. After the app is restarted, the value
/// is deserialized and passed back to the [RouterDelegate] via a call to
/// [RouterDelegate.setRestoredRoutePath] (which by default just calls
/// [RouterDelegate.setNewRoutePath]). It is the responsibility of the
/// [RouterDelegate] to use the configuration information provided to restore
/// its internal state.
///
/// To serialize [RouterDelegate.currentConfiguration] and to deserialize it
/// again, the [Router] calls [RouteInformationParser.restoreRouteInformation]
/// and [RouteInformationParser.parseRouteInformation], respectively. Therefore,
/// if a [restorationScopeId] is provided, a [routeInformationParser] must be
/// configured as well.
class Router<T> extends StatefulWidget {
/// Creates a router.
///
/// The [routeInformationProvider] and [routeInformationParser] can be null if this
/// router does not depend on route information. A common example is a sub router
/// that builds its content completely based on the app state.
///
/// The [routeInformationProvider] and [routeInformationParser] must
/// both be provided or not provided.
const Router({
super.key,
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
this.restorationScopeId,
}) : assert(
routeInformationProvider == null || routeInformationParser != null,
'A routeInformationParser must be provided when a routeInformationProvider is specified.',
);
/// Creates a router with a [RouterConfig].
///
/// The [RouterConfig.routeInformationProvider] and
/// [RouterConfig.routeInformationParser] can be null if this router does not
/// depend on route information. A common example is a sub router that builds
/// its content completely based on the app state.
///
/// If the [RouterConfig.routeInformationProvider] is not null, then
/// [RouterConfig.routeInformationParser] must also not be
/// null.
factory Router.withConfig({
Key? key,
required RouterConfig<T> config,
String? restorationScopeId,
}) {
return Router<T>(
key: key,
routeInformationProvider: config.routeInformationProvider,
routeInformationParser: config.routeInformationParser,
routerDelegate: config.routerDelegate,
backButtonDispatcher: config.backButtonDispatcher,
restorationScopeId: restorationScopeId,
);
}
/// The route information provider for the router.
///
/// The value at the time of first build will be used as the initial route.
/// The [Router] listens to this provider and rebuilds with new names when
/// it notifies.
///
/// This can be null if this router does not rely on the route information
/// to build its content. In such case, the [routeInformationParser] must also
/// be null.
final RouteInformationProvider? routeInformationProvider;
/// The route information parser for the router.
///
/// When the [Router] gets a new route information from the [routeInformationProvider],
/// the [Router] uses this delegate to parse the route information and produce a
/// configuration. The configuration will be used by [routerDelegate] and
/// eventually rebuilds the [Router] widget.
///
/// Since this delegate is the primary consumer of the [routeInformationProvider],
/// it must not be null if [routeInformationProvider] is not null.
final RouteInformationParser<T>? routeInformationParser;
/// The router delegate for the router.
///
/// This delegate consumes the configuration from [routeInformationParser] and
/// builds a navigating widget for the [Router].
///
/// It is also the primary respondent for the [backButtonDispatcher]. The
/// [Router] relies on [RouterDelegate.popRoute] to handle the back
/// button.
///
/// If the [RouterDelegate.currentConfiguration] returns a non-null object,
/// this [Router] will opt for URL updates.
final RouterDelegate<T> routerDelegate;
/// The back button dispatcher for the router.
///
/// The two common alternatives are the [RootBackButtonDispatcher] for root
/// router, or the [ChildBackButtonDispatcher] for other routers.
final BackButtonDispatcher? backButtonDispatcher;
/// Restoration ID to save and restore the state of the [Router].
///
/// If non-null, the [Router] will persist the [RouterDelegate]'s current
/// configuration (i.e. [RouterDelegate.currentConfiguration]). During state
/// restoration, the [Router] informs the [RouterDelegate] of the previous
/// configuration by calling [RouterDelegate.setRestoredRoutePath] (which by
/// default just calls [RouterDelegate.setNewRoutePath]). It is the
/// responsibility of the [RouterDelegate] to restore its internal state based
/// on the provided configuration.
///
/// The router uses the [RouteInformationParser] to serialize and deserialize
/// [RouterDelegate.currentConfiguration]. Therefore, a
/// [routeInformationParser] must be provided when [restorationScopeId] is
/// non-null.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationScopeId;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// This method provides access to the delegates in the [Router]. For example,
/// this can be used to access the [backButtonDispatcher] of the parent router
/// when creating a [ChildBackButtonDispatcher] for a nested [Router].
///
/// If no [Router] ancestor exists for the given context, this will assert in
/// debug mode, and throw an exception in release mode.
///
/// See also:
///
/// * [maybeOf], which is a similar function, but it will return null instead
/// of throwing an exception if no [Router] ancestor exists.
static Router<T> of<T extends Object?>(BuildContext context) {
final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
assert(() {
if (scope == null) {
throw FlutterError(
'Router operation requested with a context that does not include a Router.\n'
'The context used to retrieve the Router must be that of a widget that '
'is a descendant of a Router widget.',
);
}
return true;
}());
return scope!.routerState.widget as Router<T>;
}
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// This method provides access to the delegates in the [Router]. For example,
/// this can be used to access the [backButtonDispatcher] of the parent router
/// when creating a [ChildBackButtonDispatcher] for a nested [Router].
///
/// If no `Router` ancestor exists for the given context, this will return
/// null.
///
/// See also:
///
/// * [of], a similar method that returns a non-nullable value, and will
/// throw if no [Router] ancestor exists.
static Router<T>? maybeOf<T extends Object?>(BuildContext context) {
final _RouterScope? scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
return scope?.routerState.widget as Router<T>?;
}
/// Forces the [Router] to run the [callback] and create a new history
/// entry in the browser.
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will only report
/// them if it detects the [RouteInformation.uri] changes. Use this
/// method if you want the [Router] to report the route information even if
/// the location does not change. This can be useful when you want to
/// support the browser backward and forward button without changing the URL.
///
/// For example, you can store certain state such as the scroll position into
/// the [RouteInformation.state]. If you use this method to update the
/// scroll position multiple times with the same URL, the browser will create
/// a stack of new history entries with the same URL but different
/// [RouteInformation.state]s that store the new scroll positions. If the user
/// click the backward button in the browser, the browser will restore the
/// scroll positions saved in history entries without changing the URL.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [neglect]: which forces the [Router] to not create a new history entry
/// even if location does change.
static void navigate(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()!
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(RouteInformationReportingType.navigate, callback);
}
/// Forces the [Router] to run the [callback] without creating a new history
/// entry in the browser.
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will report them
/// automatically if it detects the [RouteInformation.uri] changes.
///
/// Creating a new route history entry makes users feel they have visited a
/// new page, and the browser back button brings them back to previous history
/// entry. Use this method if you don't want the [Router] to create a new
/// route information even if it detects changes as a result of running the
/// [callback].
///
/// Using this method will still update the URL and state in current history
/// entry.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [navigate]: which forces the [Router] to create a new history entry
/// even if location does not change.
static void neglect(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()!
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(RouteInformationReportingType.neglect, callback);
}
@override
State<Router<T>> createState() => _RouterState<T>();
}
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
typedef _RouteSetter<T> = Future<void> Function(T);
/// The [Router]'s intention when it reports a new [RouteInformation] to the
/// [RouteInformationProvider].
///
/// See also:
///
/// * [RouteInformationProvider.routerReportsNewRouteInformation]: which is
/// called by the router when it has a new route information to report.
enum RouteInformationReportingType {
/// Router does not have a specific intention.
///
/// The router generates a new route information every time it detects route
/// information may have change due to a rebuild. This is the default type if
/// neither [Router.neglect] nor [Router.navigate] was used during the
/// rebuild.
none,
/// The accompanying [RouteInformation] were generated during a
/// [Router.neglect] call.
neglect,
/// The accompanying [RouteInformation] were generated during a
/// [Router.navigate] call.
navigate,
}
class _RouterState<T> extends State<Router<T>> with RestorationMixin {
Object? _currentRouterTransaction;
RouteInformationReportingType? _currentIntentionToReport;
final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
late bool _routeParsePending;
@override
String? get restorationId => widget.restorationScopeId;
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_routeInformation, 'route');
if (_routeInformation.value != null) {
assert(widget.routeInformationParser != null);
_processRouteInformation(_routeInformation.value!, () => widget.routerDelegate.setRestoredRoutePath);
} else if (widget.routeInformationProvider != null) {
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setInitialRoutePath);
}
}
bool _routeInformationReportingTaskScheduled = false;
void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null) {
return;
}
assert(_currentIntentionToReport != null);
_routeInformationReportingTaskScheduled = true;
SchedulerBinding.instance.addPostFrameCallback(
_reportRouteInformation,
debugLabel: 'Router.reportRouteInfo',
);
}
void _reportRouteInformation(Duration timestamp) {
if (!mounted) {
return;
}
assert(_routeInformationReportingTaskScheduled);
_routeInformationReportingTaskScheduled = false;
if (_routeInformation.value != null) {
final RouteInformation currentRouteInformation = _routeInformation.value!;
assert(_currentIntentionToReport != null);
widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation, type: _currentIntentionToReport!);
}
_currentIntentionToReport = RouteInformationReportingType.none;
}
RouteInformation? _retrieveNewRouteInformation() {
final T? configuration = widget.routerDelegate.currentConfiguration;
if (configuration == null) {
return null;
}
return widget.routeInformationParser?.restoreRouteInformation(configuration);
}
void _setStateWithExplicitReportStatus(
RouteInformationReportingType status,
VoidCallback fn,
) {
assert(status.index >= RouteInformationReportingType.neglect.index);
assert(() {
if (_currentIntentionToReport != null &&
_currentIntentionToReport != RouteInformationReportingType.none &&
_currentIntentionToReport != status) {
FlutterError.reportError(
const FlutterErrorDetails(
exception:
'Both Router.navigate and Router.neglect have been called in this '
'build cycle, and the Router cannot decide whether to report the '
'route information. Please make sure only one of them is called '
'within the same build cycle.',
),
);
}
return true;
}());
_currentIntentionToReport = status;
_scheduleRouteInformationReportingTask();
fn();
}
void _maybeNeedToReportRouteInformation() {
_routeInformation.value = _retrieveNewRouteInformation();
_currentIntentionToReport ??= RouteInformationReportingType.none;
_scheduleRouteInformationReportingTask();
}
@override
void didChangeDependencies() {
_routeParsePending = true;
super.didChangeDependencies();
// The super.didChangeDependencies may have parsed the route information.
// This can happen if the didChangeDependencies is triggered by state
// restoration or first build.
if (widget.routeInformationProvider != null && _routeParsePending) {
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
_routeParsePending = false;
_maybeNeedToReportRouteInformation();
}
@override
void didUpdateWidget(Router<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.routeInformationProvider != oldWidget.routeInformationProvider ||
widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
widget.routeInformationParser != oldWidget.routeInformationParser ||
widget.routerDelegate != oldWidget.routerDelegate) {
_currentRouterTransaction = Object();
}
if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
if (oldWidget.routeInformationProvider?.value != widget.routeInformationProvider?.value) {
_handleRouteInformationProviderNotification();
}
}
if (widget.backButtonDispatcher != oldWidget.backButtonDispatcher) {
oldWidget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
}
if (widget.routerDelegate != oldWidget.routerDelegate) {
oldWidget.routerDelegate.removeListener(_handleRouterDelegateNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_maybeNeedToReportRouteInformation();
}
}
@override
void dispose() {
_routeInformation.dispose();
widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
_currentRouterTransaction = null;
super.dispose();
}
void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
assert(_routeParsePending);
_routeParsePending = false;
_currentRouterTransaction = Object();
widget.routeInformationParser!
.parseRouteInformationWithDependencies(information, context)
.then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
}
_RouteSetter<T> _processParsedRouteInformation(Object? transaction, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
return (T data) async {
if (_currentRouterTransaction != transaction) {
return;
}
await delegateRouteSetter()(data);
if (_currentRouterTransaction == transaction) {
_rebuild();
}
};
}
void _handleRouteInformationProviderNotification() {
_routeParsePending = true;
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
Future<bool> _handleBackButtonDispatcherNotification() {
_currentRouterTransaction = Object();
return widget.routerDelegate
.popRoute()
.then<bool>(_handleRoutePopped(_currentRouterTransaction));
}
_AsyncPassthrough<bool> _handleRoutePopped(Object? transaction) {
return (bool data) {
if (transaction != _currentRouterTransaction) {
// A rebuilt was trigger from a different source. Returns true to
// prevent bubbling.
return SynchronousFuture<bool>(true);
}
_rebuild();
return SynchronousFuture<bool>(data);
};
}
Future<void> _rebuild([void value]) {
setState(() {/* routerDelegate is ready to rebuild */});
_maybeNeedToReportRouteInformation();
return SynchronousFuture<void>(value);
}
void _handleRouterDelegateNotification() {
setState(() {/* routerDelegate wants to rebuild */});
_maybeNeedToReportRouteInformation();
}
@override
Widget build(BuildContext context) {
return UnmanagedRestorationScope(
bucket: bucket,
child: _RouterScope(
routeInformationProvider: widget.routeInformationProvider,
backButtonDispatcher: widget.backButtonDispatcher,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
routerState: this,
child: Builder(
// Use a Builder so that the build method below will have a
// BuildContext that contains the _RouterScope. This also prevents
// dependencies look ups in routerDelegate from rebuilding Router
// widget that may result in re-parsing the route information.
builder: widget.routerDelegate.build,
),
),
);
}
}
class _RouterScope extends InheritedWidget {
const _RouterScope({
required this.routeInformationProvider,
required this.backButtonDispatcher,
required this.routeInformationParser,
required this.routerDelegate,
required this.routerState,
required super.child,
}) : assert(routeInformationProvider == null || routeInformationParser != null);
final ValueListenable<RouteInformation?>? routeInformationProvider;
final BackButtonDispatcher? backButtonDispatcher;
final RouteInformationParser<Object?>? routeInformationParser;
final RouterDelegate<Object?> routerDelegate;
final _RouterState<Object?> routerState;
@override
bool updateShouldNotify(_RouterScope oldWidget) {
return routeInformationProvider != oldWidget.routeInformationProvider ||
backButtonDispatcher != oldWidget.backButtonDispatcher ||
routeInformationParser != oldWidget.routeInformationParser ||
routerDelegate != oldWidget.routerDelegate ||
routerState != oldWidget.routerState;
}
}
/// A class that can be extended or mixed in that invokes a single callback,
/// which then returns a value.
///
/// While multiple callbacks can be registered, when a notification is
/// dispatched there must be only a single callback. The return values of
/// multiple callbacks are not aggregated.
///
/// `T` is the return value expected from the callback.
///
/// See also:
///
/// * [Listenable] and its subclasses, which provide a similar mechanism for
/// one-way signaling.
class _CallbackHookProvider<T> {
final ObserverList<ValueGetter<T>> _callbacks = ObserverList<ValueGetter<T>>();
/// Whether a callback is currently registered.
@protected
bool get hasCallbacks => _callbacks.isNotEmpty;
/// Register the callback to be called when the object changes.
///
/// If other callbacks have already been registered, they must be removed
/// (with [removeCallback]) before the callback is next called.
void addCallback(ValueGetter<T> callback) => _callbacks.add(callback);
/// Remove a previously registered callback.
///
/// If the given callback is not registered, the call is ignored.
void removeCallback(ValueGetter<T> callback) => _callbacks.remove(callback);
/// Calls the (single) registered callback and returns its result.
///
/// If no callback is registered, or if the callback throws, returns
/// `defaultValue`.
///
/// Call this method whenever the callback is to be invoked. If there is more
/// than one callback registered, this method will throw a [StateError].
///
/// Exceptions thrown by callbacks will be caught and reported using
/// [FlutterError.reportError].
@protected
@pragma('vm:notify-debugger-on-exception')
T invokeCallback(T defaultValue) {
if (_callbacks.isEmpty) {
return defaultValue;
}
try {
return _callbacks.single();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widget library',
context: ErrorDescription('while invoking the callback for $runtimeType'),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<_CallbackHookProvider<T>>(
'The $runtimeType that invoked the callback was',
this,
style: DiagnosticsTreeStyle.errorProperty,
),
],
));
return defaultValue;
}
}
}
/// Report to a [Router] when the user taps the back button on platforms that
/// support back buttons (such as Android).
///
/// When [Router] widgets are nested, consider using a
/// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher],
/// so that the back button requests get dispatched to the appropriate [Router].
/// To make this work properly, it's important that whenever a [Router] thinks
/// it should get the back button messages (e.g. after the user taps inside it),
/// it calls [takePriority] on its [BackButtonDispatcher] (or
/// [ChildBackButtonDispatcher]) instance.
///
/// The class takes a single callback, which must return a [Future<bool>]. The
/// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely,
/// the callback should return a future that completes to true if it can handle
/// the pop request, and a future that completes to false otherwise.
abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
late final LinkedHashSet<ChildBackButtonDispatcher> _children =
<ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
@override
bool get hasCallbacks => super.hasCallbacks || (_children.isNotEmpty);
/// Handles a pop route request.
///
/// This method prioritizes the children list in reverse order and calls
/// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them
/// handles the request (by returning a future with true), it exits this
/// method by returning this future. Otherwise, it keeps moving on to the next
/// child until a child handles the request. If none of the children handles
/// the request, this back button dispatcher will then try to handle the request
/// by itself. This back button dispatcher handles the request by notifying the
/// router which in turn calls the [RouterDelegate.popRoute] and returns its
/// result.
///
/// To decide whether this back button dispatcher will handle the pop route
/// request, you can override the [RouterDelegate.popRoute] of the router
/// delegate you pass into the router with this back button dispatcher to
/// return a future of true or false.
@override
Future<bool> invokeCallback(Future<bool> defaultValue) {
if (_children.isNotEmpty) {
final List<ChildBackButtonDispatcher> children = _children.toList();
int childIndex = children.length - 1;
Future<bool> notifyNextChild(bool result) {
// If the previous child handles the callback, we return the result.
if (result) {
return SynchronousFuture<bool>(result);
}
// If the previous child did not handle the callback, we ask the next
// child to handle the it.
if (childIndex > 0) {
childIndex -= 1;
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
// If none of the child handles the callback, the parent will then handle it.
return super.invokeCallback(defaultValue);
}
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
return super.invokeCallback(defaultValue);
}
/// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this
/// back button dispatcher.
///
/// To participate in handling the pop route request, call the [takePriority]
/// on the [ChildBackButtonDispatcher] created from this method.
///
/// When the pop route request is handled by this back button dispatcher, it
/// propagate the request to its direct descendants that have called the
/// [takePriority] method. If there are multiple candidates, the latest one
/// that called the [takePriority] wins the right to handle the request. If
/// the latest one does not handle the request (by returning a future of
/// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest
/// one will then have the right to handle the request. This dispatcher
/// continues finding the next candidate until there are no more candidates
/// and finally handles the request itself.
ChildBackButtonDispatcher createChildBackButtonDispatcher() {
return ChildBackButtonDispatcher(this);
}
/// Make this [BackButtonDispatcher] take priority among its peers.