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