Line data Source code
1 : import 'package:beamer/beamer.dart';
2 : import 'package:beamer/src/beam_state.dart';
3 : import 'package:beamer/src/utils.dart';
4 : import 'package:flutter/widgets.dart';
5 :
6 : /// Configuration for a navigatable application region.
7 : ///
8 : /// Responsible for
9 : /// * knowing which URIs it can handle: [pathBlueprints]
10 : /// * knowing how to build a stack of pages: [buildPages]
11 : /// * keeping a [state] that provides the link between the first 2
12 : ///
13 : /// Extend this class to define your locations to which you can then beam to.
14 : abstract class BeamLocation<T extends RouteInformationSerializable>
15 : extends ChangeNotifier {
16 9 : BeamLocation([RouteInformation? routeInformation]) {
17 18 : state = createState(
18 : routeInformation ?? const RouteInformation(location: '/'),
19 : );
20 : }
21 :
22 : /// A state of this location.
23 : ///
24 : /// Upon beaming, it will be populated by all necessary attributes.
25 : /// See [BeamState].
26 : late T state;
27 :
28 : /// How to create state from generic [BeamState], that is produced
29 : /// by [BeamerDelegate] and passed via [BeamerDelegate.locationBuilder].
30 : ///
31 : /// Override this if you have your custom state class extending [BeamState].
32 9 : T createState(RouteInformation routeInformation) =>
33 9 : BeamState.fromRouteInformation(routeInformation, beamLocation: this) as T;
34 :
35 : /// Update a state via callback receiving the current state.
36 : /// If no callback is given, just notifies [BeamerDelegate] to rebuild.
37 : ///
38 : /// Useful with [BeamState.copyWith].
39 2 : void update([T Function(T)? copy, bool rebuild = true]) {
40 : if (copy != null) {
41 4 : state = copy(state);
42 : }
43 : if (rebuild) {
44 2 : notifyListeners();
45 : }
46 : }
47 :
48 : /// Can this handle the [uri] based on its [pathBlueprints].
49 : ///
50 : /// Can be useful in a custom [BeamerDelegate.locationBuilder].
51 2 : bool canHandle(Uri uri) => Utils.canBeamLocationHandleUri(this, uri);
52 :
53 : /// Gives the ability to wrap the [navigator].
54 : ///
55 : /// Mostly useful for providing something to the entire location,
56 : /// i.e. to all of the pages.
57 : ///
58 : /// For example:
59 : ///
60 : /// ```dart
61 : /// @override
62 : /// Widget builder(BuildContext context, Widget navigator) {
63 : /// return MyProvider<MyObject>(
64 : /// create: (context) => MyObject(),
65 : /// child: navigator,
66 : /// );
67 : /// }
68 : /// ```
69 6 : Widget builder(BuildContext context, Widget navigator) => navigator;
70 :
71 : /// Represents the "form" of URI paths supported by this [BeamLocation].
72 : ///
73 : /// You can pass in either a String or a RegExp. Beware of using greedy regular
74 : /// expressions as this might lead to unexpected behaviour.
75 : ///
76 : /// For strings, optional path segments are denoted with ':xxx' and consequently
77 : /// `{'xxx': <real>}` will be put to [pathParameters].
78 : /// For regular expressions we use named groups as optional path segments, following
79 : /// regex is tested to be effective in most cases `RegExp('/test/(?<test>[a-z]+){0,1}')`
80 : /// This will put `{'test': <real>}` to [pathParameters]. Note that we use the name from the regex group.
81 : ///
82 : /// Optional path segments can be used as a mean to pass data regardless of
83 : /// whether there is a browser.
84 : ///
85 : /// For example: '/books/:id' or using regex `RegExp('/test/(?<test>[a-z]+){0,1}')`
86 : List<Pattern> get pathBlueprints;
87 :
88 : /// Creates and returns the list of pages to be built by the [Navigator]
89 : /// when this [BeamLocation] is beamed to or internally inferred.
90 : ///
91 : /// [context] can be useful while building the pages.
92 : /// It will also contain anything injected via [builder].
93 : List<BeamPage> buildPages(BuildContext context, T state);
94 :
95 : /// Guards that will be executing [check] when this gets beamed to.
96 : ///
97 : /// Checks will be executed in order; chain of responsibility pattern.
98 : /// When some guard returns `false`, a candidate will not be accepted
99 : /// and stack of pages will be updated as is configured in [BeamGuard].
100 : ///
101 : /// Override this in your subclasses, if needed.
102 : /// See [BeamGuard].
103 6 : List<BeamGuard> get guards => const <BeamGuard>[];
104 :
105 : /// A transition delegate to be used by [Navigator].
106 : ///
107 : /// This will be used only by this location, unlike
108 : /// [BeamerDelegate.transitionDelegate] that will be used for all locations.
109 : ///
110 : /// This transition delegate will override the one in [BeamerDelegate].
111 : ///
112 : /// See [Navigator.transitionDelegate].
113 6 : TransitionDelegate? get transitionDelegate => null;
114 : }
115 :
116 : /// Default location to choose if requested URI doesn't parse to any location.
117 : class NotFound extends BeamLocation<BeamState> {
118 18 : NotFound({String path = '/'}) : super(RouteInformation(location: path));
119 :
120 1 : @override
121 1 : List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
122 :
123 6 : @override
124 6 : List<String> get pathBlueprints => [];
125 : }
126 :
127 : /// Empty location used to intialize a non-nullable BeamLocation variable.
128 : ///
129 : /// See [BeamerDelegate.currentBeamLocation].
130 : class EmptyBeamLocation extends BeamLocation<BeamState> {
131 1 : @override
132 1 : List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
133 :
134 7 : @override
135 7 : List<String> get pathBlueprints => [];
136 : }
137 :
138 : /// A beam location for [SimpleLocationBuilder], but can be used freely.
139 : ///
140 : /// Useful when needing a simple beam location with a single or few pages.
141 : class SimpleBeamLocation extends BeamLocation<BeamState> {
142 6 : SimpleBeamLocation({
143 : required RouteInformation routeInformation,
144 : required this.routes,
145 : this.navBuilder,
146 6 : }) : super(routeInformation);
147 :
148 : /// Map of all routes this location handles.
149 : Map<Pattern, dynamic Function(BuildContext, BeamState)> routes;
150 :
151 : /// A wrapper used as [BeamLocation.builder].
152 : Widget Function(BuildContext context, Widget navigator)? navBuilder;
153 :
154 6 : @override
155 : Widget builder(BuildContext context, Widget navigator) {
156 6 : return navBuilder?.call(context, navigator) ?? navigator;
157 : }
158 :
159 5 : int _compareKeys(dynamic a, dynamic b) {
160 : // try-catch a CastError
161 : try {
162 15 : return (a as String).length - (b as String).length;
163 1 : } on TypeError {
164 : return 1;
165 : }
166 : }
167 :
168 6 : @override
169 18 : List<Pattern> get pathBlueprints => routes.keys.toList();
170 :
171 6 : @override
172 : List<BeamPage> buildPages(BuildContext context, BeamState state) {
173 24 : final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys);
174 12 : final activeRoutes = Map.of(routes)
175 18 : ..removeWhere((key, value) => !filteredRoutes.containsKey(key));
176 12 : final sortedRoutes = activeRoutes.keys.toList()
177 16 : ..sort((a, b) => _compareKeys(a, b));
178 12 : return sortedRoutes.map<BeamPage>((route) {
179 12 : final routeElement = routes[route]!(context, state);
180 6 : if (routeElement is BeamPage) {
181 : return routeElement;
182 : } else {
183 6 : return BeamPage(
184 12 : key: ValueKey(filteredRoutes[route]),
185 : child: routeElement,
186 : );
187 : }
188 6 : }).toList();
189 : }
190 :
191 : /// Chooses all the routes that "sub-match" [state.uri] to stack their pages.
192 : ///
193 : /// If none of the routes _matches_ [state.uri], nothing will be selected
194 : /// and [BeamerDelegate] will declare that the location is [NotFound].
195 6 : static Map<Pattern, String> chooseRoutes(
196 : RouteInformation routeInformation, Iterable<Pattern> routes) {
197 6 : final matched = <Pattern, String>{};
198 : bool overrideNotFound = false;
199 12 : final uri = Uri.parse(routeInformation.location ?? '/');
200 12 : for (final route in routes) {
201 6 : if (route is String) {
202 12 : final uriPathSegments = uri.pathSegments.toList();
203 12 : final routePathSegments = Uri.parse(route).pathSegments;
204 :
205 18 : if (uriPathSegments.length < routePathSegments.length) {
206 : continue;
207 : }
208 :
209 : var checksPassed = true;
210 : var path = '';
211 18 : for (int i = 0; i < routePathSegments.length; i++) {
212 18 : path += '/${uriPathSegments[i]}';
213 :
214 12 : if (routePathSegments[i] == '*') {
215 : overrideNotFound = true;
216 : continue;
217 : }
218 12 : if (routePathSegments[i].startsWith(':')) {
219 : continue;
220 : }
221 18 : if (routePathSegments[i] != uriPathSegments[i]) {
222 : checksPassed = false;
223 : break;
224 : }
225 : }
226 :
227 : if (checksPassed) {
228 12 : matched[route] = Uri(
229 6 : path: path == '' ? '/' : path,
230 : queryParameters:
231 13 : uri.queryParameters.isEmpty ? null : uri.queryParameters,
232 6 : ).toString();
233 : }
234 : } else {
235 1 : final regexp = Utils.tryCastToRegExp(route);
236 2 : if (regexp.hasMatch(uri.toString())) {
237 1 : final path = uri.toString();
238 2 : matched[regexp] = Uri(
239 1 : path: path == '' ? '/' : path,
240 : queryParameters:
241 2 : uri.queryParameters.isEmpty ? null : uri.queryParameters,
242 1 : ).toString();
243 : }
244 : }
245 : }
246 :
247 : bool isNotFound = true;
248 12 : matched.forEach((key, value) {
249 6 : if (Utils.urisMatch(key, uri)) {
250 : isNotFound = false;
251 : }
252 : });
253 :
254 : if (overrideNotFound) {
255 : return matched;
256 : }
257 :
258 3 : return isNotFound ? {} : matched;
259 : }
260 : }
|