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