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