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 : /// Parameters used while beaming.
7 : class BeamParameters {
8 : /// Creates a [BeamParameters] with specified properties.
9 : ///
10 : /// All attributes can be null.
11 18 : const BeamParameters({
12 : this.transitionDelegate = const DefaultTransitionDelegate(),
13 : this.popConfiguration,
14 : this.beamBackOnPop = false,
15 : this.popBeamLocationOnPop = false,
16 : this.stacked = true,
17 : });
18 :
19 : /// Which transition delegate to use when building pages.
20 : final TransitionDelegate transitionDelegate;
21 :
22 : /// Which route to pop to, instead of default pop.
23 : ///
24 : /// This is more general than [beamBackOnPop].
25 : final RouteInformation? popConfiguration;
26 :
27 : /// Whether to implicitly [BeamerDelegate.beamBack] instead of default pop.
28 : final bool beamBackOnPop;
29 :
30 : /// Whether to remove entire current [BeamLocation] from history,
31 : /// instead of default pop.
32 : final bool popBeamLocationOnPop;
33 :
34 : /// Whether all the pages produced by [BeamLocation.buildPages] are stacked.
35 : /// If not (`false`), just the last page is taken.
36 : final bool stacked;
37 :
38 : /// Returns a copy of this with optional changes.
39 6 : BeamParameters copyWith({
40 : TransitionDelegate? transitionDelegate,
41 : RouteInformation? popConfiguration,
42 : bool? beamBackOnPop,
43 : bool? popBeamLocationOnPop,
44 : bool? stacked,
45 : }) {
46 6 : return BeamParameters(
47 4 : transitionDelegate: transitionDelegate ?? this.transitionDelegate,
48 6 : popConfiguration: popConfiguration ?? this.popConfiguration,
49 5 : beamBackOnPop: beamBackOnPop ?? this.beamBackOnPop,
50 5 : popBeamLocationOnPop: popBeamLocationOnPop ?? this.popBeamLocationOnPop,
51 5 : stacked: stacked ?? this.stacked,
52 : );
53 : }
54 : }
55 :
56 : /// An element of [BeamLocation.history] list.
57 : ///
58 : /// Contains the [RouteInformation] and [BeamParameters] at the moment
59 : /// of beaming to it.
60 : class HistoryElement {
61 : /// Creates a [HistoryElement] with specified properties.
62 : ///
63 : /// [routeInformation] must not be null.
64 9 : const HistoryElement(
65 : this.routeInformation, [
66 : this.parameters = const BeamParameters(),
67 : ]);
68 :
69 : /// A [RouteInformation] of this history entry.
70 : final RouteInformation routeInformation;
71 :
72 : /// Parameters that were used during beaming.
73 : final BeamParameters parameters;
74 : }
75 :
76 : /// Configuration for a navigatable application region.
77 : ///
78 : /// Responsible for
79 : /// * knowing which URIs it can handle: [pathPatterns]
80 : /// * knowing how to build a stack of pages: [buildPages]
81 : /// * keeping a [state] that provides the link between the first 2
82 : ///
83 : /// Extend this class to define your locations to which you can then beam to.
84 : abstract class BeamLocation<T extends RouteInformationSerializable>
85 : extends ChangeNotifier {
86 : /// Creates a [BeamLocation] with specified properties.
87 : ///
88 : /// All attributes can be null.
89 9 : BeamLocation([
90 : RouteInformation? routeInformation,
91 : BeamParameters? beamParameters,
92 : ]) {
93 9 : create(routeInformation, beamParameters);
94 : }
95 :
96 : /// A state of this [BeamLocation].
97 : ///
98 : /// Upon beaming, it will be populated by all necessary attributes.
99 : /// See also: [BeamState].
100 : late T state;
101 :
102 : /// Beam parameters used to beam to the current [state].
103 0 : BeamParameters get beamParameters => history.last.parameters;
104 :
105 : /// An arbitrary data to be stored in this.
106 : /// This will persist while navigating within this [BeamLocation].
107 : ///
108 : /// Therefore, in the case of using [RoutesLocationBuilder] which uses only
109 : /// a single [RoutesBeamLocation] for all page stacks, this data will
110 : /// be available always, until overriden with some new data.
111 : Object? data;
112 :
113 : /// Whether [buildInit] was called.
114 : ///
115 : /// See [buildInit].
116 : bool mounted = false;
117 :
118 : /// Whether this [BeamLocation] is currently in use by [BeamerDelegate].
119 : ///
120 : /// This influences on the behavior of [create] that gets called on existing
121 : /// [BeamLocation]s when using [BeamerLocationBuilder] that uses [Utils.chooseBeamLocation].
122 : bool isCurrent = false;
123 :
124 : /// Creates the [state] and adds the [routeInformation] to [history].
125 : /// This is called only once during the lifetime of [BeamLocation].
126 : ///
127 : /// See [createState] and [addToHistory].
128 9 : void create([
129 : RouteInformation? routeInformation,
130 : BeamParameters? beamParameters,
131 : bool tryPoppingHistory = true,
132 : ]) {
133 9 : if (!isCurrent) {
134 : try {
135 9 : disposeState();
136 : } catch (e) {
137 : //
138 : }
139 18 : history.clear();
140 : }
141 18 : state = createState(
142 : routeInformation ?? const RouteInformation(location: '/'),
143 : );
144 9 : addToHistory(
145 18 : state.routeInformation,
146 : beamParameters ?? const BeamParameters(),
147 : tryPoppingHistory,
148 : );
149 : }
150 :
151 : /// How to create state from [RouteInformation] given by
152 : /// [BeamerDelegate] and passed via [BeamerDelegate.locationBuilder].
153 : ///
154 : /// This will be called only once during the lifetime of [BeamLocation].
155 : /// One should override this if using a custom state class.
156 : ///
157 : /// See [create].
158 9 : T createState(RouteInformation routeInformation) =>
159 9 : BeamState.fromRouteInformation(
160 : routeInformation,
161 : beamLocation: this,
162 : ) as T;
163 :
164 : /// What to do on state initalization.
165 : ///
166 : /// For example, add listeners to [state] if it's a [ChangeNotifier].
167 6 : void initState() {}
168 :
169 : /// Updates the [state] upon recieving new [RouteInformation], which usually
170 : /// happens after [BeamerDelegate.setNewRoutePath].
171 : ///
172 : /// Override this if you are using custom state whose copying
173 : /// should be handled customly.
174 : ///
175 : /// See [update].
176 6 : void updateState(RouteInformation routeInformation) {
177 12 : state = createState(routeInformation);
178 : }
179 :
180 : /// How to relase any resources used by [state].
181 : ///
182 : /// Override this if
183 : /// e.g. using a custom [ChangeNotifier] [state] to remove listeners.
184 9 : void disposeState() {}
185 :
186 : /// Updates the [state] and [history], depending on inputs.
187 : ///
188 : /// If [copy] function is provided, state should be created from given current [state].
189 : /// New [routeInformation] gets added to history.
190 : ///
191 : /// If [copy] is `null`, then [routeInformation] is used, either `null` or not.
192 : /// If [routeInformation] is `null`, then the state will upadate from
193 : /// last [history] element and nothing shall be added to [history].
194 : /// Else, the state updates from available [routeInformation].
195 : ///
196 : /// See [updateState] and [addToHistory].
197 6 : void update([
198 : T Function(T)? copy,
199 : RouteInformation? routeInformation,
200 : BeamParameters? beamParameters,
201 : bool rebuild = true,
202 : bool tryPoppingHistory = true,
203 : ]) {
204 : if (copy != null) {
205 4 : state = copy(state);
206 2 : addToHistory(
207 4 : state.routeInformation,
208 : beamParameters ?? const BeamParameters(),
209 : tryPoppingHistory,
210 : );
211 : } else {
212 : if (routeInformation == null) {
213 16 : updateState(history.last.routeInformation);
214 : } else {
215 6 : updateState(routeInformation);
216 6 : addToHistory(
217 12 : state.routeInformation,
218 : beamParameters ?? const BeamParameters(),
219 : tryPoppingHistory,
220 : );
221 : }
222 : }
223 : if (rebuild) {
224 2 : notifyListeners();
225 : }
226 : }
227 :
228 : /// The history of beaming for this.
229 : List<HistoryElement> history = [];
230 :
231 : /// Adds another [HistoryElement] to [history] list.
232 : /// The history element is created from given [state] and [beamParameters].
233 : ///
234 : /// If [tryPopping] is set to `true`, the state with the same `location`
235 : /// will be searched in [history] and if found, entire history segment
236 : /// `[foundIndex, history.length-1]` will be removed before adding a new
237 : /// history element.
238 9 : void addToHistory(
239 : RouteInformation routeInformation, [
240 : BeamParameters beamParameters = const BeamParameters(),
241 : bool tryPopping = true,
242 : ]) {
243 : if (tryPopping) {
244 24 : final sameStateIndex = history.indexWhere((element) {
245 18 : return element.routeInformation.location ==
246 18 : state.routeInformation.location;
247 : });
248 18 : if (sameStateIndex != -1) {
249 16 : history.removeRange(sameStateIndex, history.length);
250 : }
251 : }
252 18 : if (history.isEmpty ||
253 36 : routeInformation.location != history.last.routeInformation.location) {
254 27 : history.add(HistoryElement(routeInformation, beamParameters));
255 : }
256 : }
257 :
258 : /// Removes the last [HistoryElement] from [history] and returns it.
259 : ///
260 : /// If said history element is a `ChangeNotifier`, listeners are removed.
261 4 : HistoryElement? removeLastFromHistory() {
262 8 : if (history.isEmpty) {
263 : return null;
264 : }
265 8 : return history.removeLast();
266 : }
267 :
268 : /// Initialize custom bindings for this [BeamLocation] using [BuildContext].
269 : /// Similar to [builder], but is not tied to Widget tree.
270 : ///
271 : /// This will be called on just the first build of this [BeamLocation]
272 : /// and sets [mounted] to true. It is called right before [buildPages].
273 6 : @mustCallSuper
274 : void buildInit(BuildContext context) {
275 6 : mounted = true;
276 : }
277 :
278 : /// Can this handle the [uri] based on its [pathPatterns].
279 : ///
280 : /// Can be useful in a custom [BeamerDelegate.locationBuilder].
281 2 : bool canHandle(Uri uri) => Utils.canBeamLocationHandleUri(this, uri);
282 :
283 : /// Gives the ability to wrap the [navigator].
284 : ///
285 : /// Mostly useful for providing something to the entire location,
286 : /// i.e. to all of the pages.
287 : ///
288 : /// For example:
289 : ///
290 : /// ```dart
291 : /// @override
292 : /// Widget builder(BuildContext context, Widget navigator) {
293 : /// return MyProvider<MyObject>(
294 : /// create: (context) => MyObject(),
295 : /// child: navigator,
296 : /// );
297 : /// }
298 : /// ```
299 6 : Widget builder(BuildContext context, Widget navigator) => navigator;
300 :
301 : /// Represents the "form" of URI paths supported by this [BeamLocation].
302 : ///
303 : /// You can pass in either a String or a RegExp. Beware of using greedy regular
304 : /// expressions as this might lead to unexpected behaviour.
305 : ///
306 : /// For strings, optional path segments are denoted with ':xxx' and consequently
307 : /// `{'xxx': <real>}` will be put to [pathParameters].
308 : /// For regular expressions we use named groups as optional path segments, following
309 : /// regex is tested to be effective in most cases `RegExp('/test/(?<test>[a-z]+){0,1}')`
310 : /// This will put `{'test': <real>}` to [pathParameters]. Note that we use the name from the regex group.
311 : ///
312 : /// Optional path segments can be used as a mean to pass data regardless of
313 : /// whether there is a browser.
314 : ///
315 : /// For example: '/books/:id' or using regex `RegExp('/test/(?<test>[a-z]+){0,1}')`
316 : List<Pattern> get pathPatterns;
317 :
318 : /// Creates and returns the list of pages to be built by the [Navigator]
319 : /// when this [BeamLocation] is beamed to or internally inferred.
320 : ///
321 : /// [context] can be useful while building the pages.
322 : /// It will also contain anything injected via [builder].
323 : List<BeamPage> buildPages(BuildContext context, T state);
324 :
325 : /// Guards that will be executing [check] when this gets beamed to.
326 : ///
327 : /// Checks will be executed in order; chain of responsibility pattern.
328 : /// When some guard returns `false`, a candidate will not be accepted
329 : /// and stack of pages will be updated as is configured in [BeamGuard].
330 : ///
331 : /// Override this in your subclasses, if needed.
332 : /// See [BeamGuard].
333 6 : List<BeamGuard> get guards => const <BeamGuard>[];
334 :
335 : /// A transition delegate to be used by [Navigator].
336 : ///
337 : /// This will be used only by this location, unlike
338 : /// [BeamerDelegate.transitionDelegate] that will be used for all locations.
339 : ///
340 : /// This transition delegate will override the one in [BeamerDelegate].
341 : ///
342 : /// See [Navigator.transitionDelegate].
343 6 : TransitionDelegate? get transitionDelegate => null;
344 : }
345 :
346 : /// Default location to choose if requested URI doesn't parse to any location.
347 : class NotFound extends BeamLocation<BeamState> {
348 : /// Creates a [NotFound] [BeamLocation] with
349 : /// `RouteInformation(location: path)` as its state.
350 18 : NotFound({String path = '/'}) : super(RouteInformation(location: path));
351 :
352 1 : @override
353 1 : List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
354 :
355 6 : @override
356 6 : List<String> get pathPatterns => [];
357 : }
358 :
359 : /// Empty location used to intialize a non-nullable BeamLocation variable.
360 : ///
361 : /// See [BeamerDelegate.currentBeamLocation].
362 : class EmptyBeamLocation extends BeamLocation<BeamState> {
363 1 : @override
364 1 : List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
365 :
366 7 : @override
367 7 : List<String> get pathPatterns => [];
368 : }
369 :
370 : /// A beam location for [RoutesLocationBuilder], but can be used freely.
371 : ///
372 : /// Useful when needing a simple beam location with a single or few pages.
373 : class RoutesBeamLocation extends BeamLocation<BeamState> {
374 : /// Creates a [RoutesBeamLocation] with specified properties.
375 : ///
376 : /// [routeInformation] and [routes] are required.
377 6 : RoutesBeamLocation({
378 : required RouteInformation routeInformation,
379 : Object? data,
380 : BeamParameters? beamParameters,
381 : required this.routes,
382 : this.navBuilder,
383 6 : }) : super(routeInformation, beamParameters);
384 :
385 : /// Map of all routes this location handles.
386 : Map<Pattern, dynamic Function(BuildContext, BeamState, Object? data)> routes;
387 :
388 : /// A wrapper used as [BeamLocation.builder].
389 : Widget Function(BuildContext context, Widget navigator)? navBuilder;
390 :
391 6 : @override
392 : Widget builder(BuildContext context, Widget navigator) {
393 6 : return navBuilder?.call(context, navigator) ?? navigator;
394 : }
395 :
396 5 : int _compareKeys(Pattern a, Pattern b) {
397 5 : if (a is RegExp && b is RegExp) {
398 0 : return a.pattern.length - b.pattern.length;
399 : }
400 5 : if (a is RegExp && b is String) {
401 0 : return a.pattern.length - b.length;
402 : }
403 10 : if (a is String && b is RegExp) {
404 4 : return a.length - b.pattern.length;
405 : }
406 10 : if (a is String && b is String) {
407 15 : return a.length - b.length;
408 : }
409 : return 0;
410 : }
411 :
412 6 : @override
413 18 : List<Pattern> get pathPatterns => routes.keys.toList();
414 :
415 6 : @override
416 : List<BeamPage> buildPages(BuildContext context, BeamState state) {
417 24 : final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys);
418 12 : final routeBuilders = Map.of(routes)
419 18 : ..removeWhere((key, value) => !filteredRoutes.containsKey(key));
420 12 : final sortedRoutes = routeBuilders.keys.toList()
421 16 : ..sort((a, b) => _compareKeys(a, b));
422 12 : final pages = sortedRoutes.map<BeamPage>((route) {
423 18 : final routeElement = routes[route]!(context, state, data);
424 6 : if (routeElement is BeamPage) {
425 : return routeElement;
426 : } else {
427 6 : return BeamPage(
428 12 : key: ValueKey(filteredRoutes[route]),
429 : child: routeElement,
430 : );
431 : }
432 6 : }).toList();
433 : return pages;
434 : }
435 :
436 : /// Chooses all the routes that "sub-match" [state.routeInformation] to stack their pages.
437 : ///
438 : /// If none of the routes _matches_ [state.uri], nothing will be selected
439 : /// and [BeamerDelegate] will declare that the location is [NotFound].
440 6 : static Map<Pattern, String> chooseRoutes(
441 : RouteInformation routeInformation,
442 : Iterable<Pattern> routes,
443 : ) {
444 6 : final matched = <Pattern, String>{};
445 : bool overrideNotFound = false;
446 12 : final uri = Uri.parse(routeInformation.location ?? '/');
447 12 : for (final route in routes) {
448 6 : if (route is String) {
449 12 : final uriPathSegments = uri.pathSegments.toList();
450 12 : final routePathSegments = Uri.parse(route).pathSegments;
451 :
452 18 : if (uriPathSegments.length < routePathSegments.length) {
453 : continue;
454 : }
455 :
456 : var checksPassed = true;
457 : var path = '';
458 18 : for (int i = 0; i < routePathSegments.length; i++) {
459 18 : path += '/${uriPathSegments[i]}';
460 :
461 12 : if (routePathSegments[i] == '*') {
462 : overrideNotFound = true;
463 : continue;
464 : }
465 12 : if (routePathSegments[i].startsWith(':')) {
466 : continue;
467 : }
468 18 : if (routePathSegments[i] != uriPathSegments[i]) {
469 : checksPassed = false;
470 : break;
471 : }
472 : }
473 :
474 : if (checksPassed) {
475 12 : matched[route] = Uri(
476 6 : path: path == '' ? '/' : path,
477 : queryParameters:
478 14 : uri.queryParameters.isEmpty ? null : uri.queryParameters,
479 6 : ).toString();
480 : }
481 : } else {
482 1 : final regexp = Utils.tryCastToRegExp(route);
483 2 : if (regexp.hasMatch(uri.toString())) {
484 1 : final path = uri.toString();
485 2 : matched[regexp] = Uri(
486 1 : path: path == '' ? '/' : path,
487 : queryParameters:
488 2 : uri.queryParameters.isEmpty ? null : uri.queryParameters,
489 1 : ).toString();
490 : }
491 : }
492 : }
493 :
494 : bool isNotFound = true;
495 12 : matched.forEach((key, value) {
496 6 : if (Utils.urisMatch(key, uri)) {
497 : isNotFound = false;
498 : }
499 : });
500 :
501 : if (overrideNotFound) {
502 : return matched;
503 : }
504 :
505 3 : return isNotFound ? {} : matched;
506 : }
507 : }
|