Line data Source code
1 : part of '../main.dart';
2 :
3 : /// A [VRouteElement] similar to [VNester] but which allows you to specify your own page
4 : /// thanks to [pageBuilder]
5 : class VNesterPageBase extends VRouteElement with VoidVGuard, VoidVPopHandler {
6 : /// A list of [VRouteElement] which widget will be accessible in [widgetBuilder]
7 : final List<VRouteElement> nestedRoutes;
8 :
9 : /// The list of possible routes to stack on top of this [VRouteElement]
10 : final List<VRouteElement> stackedRoutes;
11 :
12 : /// A function which creates the [VRouteElement._rootVRouter] associated to this [VRouteElement]
13 : ///
14 : /// [child] will be the [VRouteElement._rootVRouter] of the matched [VRouteElement] in
15 : /// [nestedRoutes]
16 : final Widget Function(Widget child) widgetBuilder;
17 :
18 : /// A LocalKey that will be given to the page which contains the given [_rootVRouter]
19 : ///
20 : /// This key mostly controls the page animation. If a page remains the same but the key is changes,
21 : /// the page gets animated
22 : /// The key is by default the value of the current [path] (or [aliases]) with
23 : /// the path parameters replaced
24 : ///
25 : /// Do provide a constant [key] if you don't want this page to animate even if [path] or
26 : /// [aliases] path parameters change
27 : final LocalKey? key;
28 :
29 : /// A name for the route which will allow you to easily navigate to it
30 : /// using [VRouter.of(context).pushNamed]
31 : ///
32 : /// Note that [name] should be unique w.r.t every [VRouteElement]
33 : final String? name;
34 :
35 : /// Function which returns a page that will wrap [widget]
36 : /// - key and name should be given to your [Page]
37 : /// - child should be placed as the last child in [Route]
38 : final Page Function(LocalKey key, Widget child, String? name) pageBuilder;
39 :
40 4 : VNesterPageBase({
41 : required this.nestedRoutes,
42 : required this.widgetBuilder,
43 : required this.pageBuilder,
44 : this.stackedRoutes = const [],
45 : this.key,
46 : this.name,
47 : }) {
48 8 : assert(nestedRoutes.isNotEmpty,
49 : 'The nestedRoutes of a VNester should not be null, otherwise it can\'t nest');
50 : }
51 :
52 : /// A key for the navigator
53 : /// It is created automatically
54 6 : late final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(
55 6 : debugLabel: 'This is the key of VNesterBase with key $key');
56 :
57 : /// A hero controller for the navigator
58 : /// It is created automatically
59 : final HeroController heroController = HeroController();
60 :
61 3 : @override
62 : VRoute? buildRoute(
63 : VPathRequestData vPathRequestData, {
64 : required VPathMatch parentVPathMatch,
65 : }) {
66 : // Set localPath to null since a VNesterPageBase marks a limit between localPaths
67 3 : VPathMatch newVPathMatch = (parentVPathMatch is ValidVPathMatch)
68 3 : ? ValidVPathMatch(
69 3 : remainingPath: parentVPathMatch.remainingPath,
70 3 : pathParameters: parentVPathMatch.pathParameters,
71 : localPath: null,
72 : )
73 2 : : InvalidVPathMatch(localPath: null);
74 :
75 : // Try to find valid VRoute from nestedRoutes
76 : VRoute? nestedRouteVRoute;
77 6 : for (var vRouteElement in nestedRoutes) {
78 3 : nestedRouteVRoute = vRouteElement.buildRoute(
79 : vPathRequestData,
80 : parentVPathMatch: newVPathMatch,
81 : );
82 : if (nestedRouteVRoute != null) {
83 : break;
84 : }
85 : }
86 :
87 : // If no child route match, this is not a match
88 : if (nestedRouteVRoute == null) {
89 : return null;
90 : }
91 :
92 : // Else also try to match stackedRoutes
93 : VRoute? stackedRouteVRoute;
94 5 : for (var vRouteElement in stackedRoutes) {
95 2 : stackedRouteVRoute = vRouteElement.buildRoute(
96 : vPathRequestData,
97 : parentVPathMatch: newVPathMatch,
98 : );
99 : if (stackedRouteVRoute != null) {
100 : break;
101 : }
102 : }
103 :
104 3 : final vRouteElementNode = VRouteElementNode(
105 : this,
106 : localPath: null,
107 3 : nestedVRouteElementNode: nestedRouteVRoute.vRouteElementNode,
108 2 : stackedVRouteElementNode: stackedRouteVRoute?.vRouteElementNode,
109 : );
110 :
111 3 : final pathParameters = {
112 3 : ...nestedRouteVRoute.pathParameters,
113 8 : ...stackedRouteVRoute?.pathParameters ?? {},
114 : };
115 :
116 3 : return VRoute(
117 : vRouteElementNode: vRouteElementNode,
118 3 : pages: [
119 6 : pageBuilder(
120 9 : key ?? ValueKey(parentVPathMatch.localPath),
121 3 : LocalVRouterData(
122 3 : child: NotificationListener<VWidgetGuardMessage>(
123 : // This listen to [VWidgetGuardNotification] which is a notification
124 : // that a [VWidgetGuard] sends when it is created
125 : // When this happens, we store the VWidgetGuard and its context
126 : // This will be used to call its afterUpdate and beforeLeave in particular.
127 1 : onNotification: (VWidgetGuardMessage vWidgetGuardMessage) {
128 1 : VWidgetGuardMessageRoot(
129 1 : vWidgetGuard: vWidgetGuardMessage.vWidgetGuard,
130 1 : localContext: vWidgetGuardMessage.localContext,
131 : associatedVRouteElement: this,
132 2 : ).dispatch(vPathRequestData.rootVRouterContext);
133 :
134 : return true;
135 : },
136 6 : child: widgetBuilder(
137 3 : Builder(
138 3 : builder: (BuildContext context) {
139 3 : return VRouterHelper(
140 6 : pages: nestedRouteVRoute!.pages.isNotEmpty
141 3 : ? nestedRouteVRoute.pages
142 0 : : [
143 0 : MaterialPage(
144 0 : child: Center(
145 0 : child: CircularProgressIndicator())),
146 : ],
147 3 : navigatorKey: navigatorKey,
148 9 : observers: <NavigatorObserver>[heroController] +
149 9 : RootVRouterData.of(context)._state.navigatorObservers,
150 3 : backButtonDispatcher: ChildBackButtonDispatcher(
151 6 : Router.of(context).backButtonDispatcher!),
152 2 : onPopPage: (_, __) {
153 4 : RootVRouterData.of(context).popFromElement(
154 2 : nestedRouteVRoute!.vRouteElementNode
155 2 : .getVRouteElementToPop(),
156 4 : pathParameters: VRouter.of(context).pathParameters,
157 : );
158 :
159 : // We always prevent popping because we handle it in VRouter
160 : return false;
161 : },
162 0 : onSystemPopPage: () async {
163 0 : await RootVRouterData.of(context).systemPopFromElement(
164 0 : nestedRouteVRoute!.vRouteElementNode
165 0 : .getVRouteElementToPop(),
166 0 : pathParameters: VRouter.of(context).pathParameters,
167 : );
168 :
169 : // We always prevent popping because we handle it in VRouter
170 : return true;
171 : },
172 : );
173 : },
174 : ),
175 : ),
176 : ),
177 : vRouteElementNode: vRouteElementNode,
178 3 : url: vPathRequestData.url,
179 3 : previousUrl: vPathRequestData.previousUrl,
180 3 : historyState: vPathRequestData.historyState,
181 : pathParameters: pathParameters,
182 3 : queryParameters: vPathRequestData.queryParameters,
183 3 : context: vPathRequestData.rootVRouterContext,
184 : ),
185 6 : name ?? parentVPathMatch.localPath,
186 : ),
187 8 : ...stackedRouteVRoute?.pages ?? [],
188 : ],
189 3 : pathParameters: nestedRouteVRoute.pathParameters,
190 6 : vRouteElements: <VRouteElement>[this] +
191 6 : nestedRouteVRoute.vRouteElements +
192 5 : (stackedRouteVRoute?.vRouteElements ?? []),
193 : );
194 : }
195 :
196 2 : GetPathFromNameResult getPathFromName(
197 : String nameToMatch, {
198 : required Map<String, String> pathParameters,
199 : required GetNewParentPathResult parentPathResult,
200 : required Map<String, String> remainingPathParameters,
201 : }) {
202 2 : final childNameResults = <GetPathFromNameResult>[];
203 :
204 : // Check if any nestedRoute matches the name
205 4 : for (var vRouteElement in nestedRoutes) {
206 2 : childNameResults.add(
207 2 : vRouteElement.getPathFromName(
208 : nameToMatch,
209 : pathParameters: pathParameters,
210 : parentPathResult: parentPathResult,
211 : remainingPathParameters: remainingPathParameters,
212 : ),
213 : );
214 4 : if (childNameResults.last is ValidNameResult) {
215 1 : return childNameResults.last;
216 : }
217 : }
218 :
219 : // Check if any subroute matches the name
220 3 : for (var vRouteElement in stackedRoutes) {
221 1 : childNameResults.add(
222 1 : vRouteElement.getPathFromName(
223 : nameToMatch,
224 : pathParameters: pathParameters,
225 : parentPathResult: parentPathResult,
226 : remainingPathParameters: remainingPathParameters,
227 : ),
228 : );
229 2 : if (childNameResults.last is ValidNameResult) {
230 1 : return childNameResults.last;
231 : }
232 : }
233 :
234 : // If no subroute or stackedRoute matches the name, try to match this name
235 4 : if (name == nameToMatch) {
236 : // If path or any alias is valid considering the given path parameters, return this
237 2 : if (parentPathResult is ValidParentPathResult) {
238 2 : if (parentPathResult.path == null) {
239 : // If this path is null, we add a NullPathErrorNameResult
240 0 : childNameResults.add(NullPathErrorNameResult(name: nameToMatch));
241 : } else {
242 2 : if (remainingPathParameters.isNotEmpty) {
243 : // If there are path parameters remaining, wee add a PathParamsErrorsNameResult
244 0 : childNameResults.add(
245 0 : PathParamsErrorsNameResult(
246 : name: nameToMatch,
247 0 : values: [
248 0 : OverlyPathParamsError(
249 0 : pathParams: pathParameters.keys.toList(),
250 : expectedPathParams:
251 0 : parentPathResult.pathParameters.keys.toList(),
252 : ),
253 : ],
254 : ),
255 : );
256 : } else {
257 : // Else the result is valid
258 4 : return ValidNameResult(path: parentPathResult.path!);
259 : }
260 : }
261 : } else {
262 2 : assert(parentPathResult is PathParamsErrorNewParentPath);
263 2 : childNameResults.add(
264 2 : PathParamsErrorsNameResult(
265 : name: nameToMatch,
266 2 : values: [
267 2 : MissingPathParamsError(
268 4 : pathParams: pathParameters.keys.toList(),
269 : missingPathParams:
270 : (parentPathResult as PathParamsErrorNewParentPath)
271 2 : .pathParameters,
272 : ),
273 : ],
274 : ),
275 : );
276 : }
277 : }
278 :
279 : // If we don't have any valid result
280 :
281 : // If some stackedRoute returned PathParamsPopError, aggregate them
282 2 : final pathParamsNameErrors = PathParamsErrorsNameResult(
283 : name: nameToMatch,
284 2 : values: childNameResults.fold<List<PathParamsError>>(
285 2 : <PathParamsError>[],
286 2 : (previousValue, element) {
287 2 : return previousValue +
288 6 : ((element is PathParamsErrorsNameResult) ? element.values : []);
289 : },
290 : ),
291 : );
292 :
293 : // If there was any PathParamsPopError, we have some pathParamsPopErrors.values
294 : // and therefore should return this
295 4 : if (pathParamsNameErrors.values.isNotEmpty) {
296 : return pathParamsNameErrors;
297 : }
298 :
299 : // Else try to find a NullPathError
300 0 : if (childNameResults.indexWhere(
301 0 : (childNameResult) => childNameResult is NullPathErrorNameResult) !=
302 0 : -1) {
303 0 : return NullPathErrorNameResult(name: nameToMatch);
304 : }
305 :
306 : // Else return a NotFoundError
307 0 : return NotFoundErrorNameResult(name: nameToMatch);
308 : }
309 :
310 3 : GetPathFromPopResult getPathFromPop(
311 : VRouteElement elementToPop, {
312 : required Map<String, String> pathParameters,
313 : required GetNewParentPathResult parentPathResult,
314 : }) {
315 : // If vRouteElement is this, then this is the element to pop so we return null
316 3 : if (elementToPop == this) {
317 0 : if (parentPathResult is ValidParentPathResult) {
318 0 : return FoundPopResult(
319 0 : path: parentPathResult.path,
320 : didPop: true,
321 0 : poppedVRouteElements: [this],
322 : );
323 : } else {
324 0 : assert(parentPathResult is PathParamsErrorNewParentPath);
325 0 : return PathParamsPopErrors(
326 0 : values: [
327 0 : MissingPathParamsError(
328 0 : pathParams: pathParameters.keys.toList(),
329 : missingPathParams:
330 : (parentPathResult as PathParamsErrorNewParentPath)
331 0 : .pathParameters,
332 : ),
333 : ],
334 : );
335 : }
336 : }
337 :
338 3 : final popErrorResults = <GetPathFromPopResult>[];
339 :
340 : // Try to pop from the nestedRoutes
341 6 : for (var vRouteElement in nestedRoutes) {
342 3 : GetPathFromPopResult childPopResult = vRouteElement.getPathFromPop(
343 : elementToPop,
344 : pathParameters: pathParameters,
345 : parentPathResult: parentPathResult,
346 : );
347 3 : if (childPopResult is FoundPopResult) {
348 3 : if (childPopResult.didPop) {
349 : // if the nestedRoute popped, we should pop too
350 3 : if (parentPathResult is ValidParentPathResult) {
351 3 : return FoundPopResult(
352 3 : path: parentPathResult.path,
353 : didPop: true,
354 : poppedVRouteElements:
355 9 : <VRouteElement>[this] + childPopResult.poppedVRouteElements,
356 : );
357 : } else {
358 0 : assert(parentPathResult is PathParamsErrorNewParentPath);
359 0 : popErrorResults.add(
360 0 : PathParamsPopErrors(
361 0 : values: [
362 0 : MissingPathParamsError(
363 0 : pathParams: pathParameters.keys.toList(),
364 : missingPathParams:
365 : (parentPathResult as PathParamsErrorNewParentPath)
366 0 : .pathParameters,
367 : ),
368 : ],
369 : ),
370 : );
371 : }
372 : } else {
373 3 : return FoundPopResult(
374 3 : path: childPopResult.path,
375 : didPop: false,
376 3 : poppedVRouteElements: childPopResult.poppedVRouteElements,
377 : );
378 : }
379 : } else {
380 2 : popErrorResults.add(childPopResult);
381 : }
382 : }
383 :
384 : // Try to pop from the subRoutes
385 4 : for (var vRouteElement in stackedRoutes) {
386 2 : GetPathFromPopResult childPopResult = vRouteElement.getPathFromPop(
387 : elementToPop,
388 : pathParameters: pathParameters,
389 : parentPathResult: parentPathResult,
390 : );
391 2 : if (childPopResult is FoundPopResult) {
392 2 : return FoundPopResult(
393 2 : path: childPopResult.path,
394 : didPop: false,
395 2 : poppedVRouteElements: childPopResult.poppedVRouteElements,
396 : );
397 : } else {
398 0 : popErrorResults.add(childPopResult);
399 : }
400 : }
401 :
402 : // If we don't have any valid result
403 :
404 : // If some stackedRoute returned PathParamsPopError, aggregate them
405 2 : final pathParamsPopErrors = PathParamsPopErrors(
406 2 : values: popErrorResults.fold<List<MissingPathParamsError>>(
407 2 : <MissingPathParamsError>[],
408 2 : (previousValue, element) {
409 2 : return previousValue +
410 4 : ((element is PathParamsPopErrors) ? element.values : []);
411 : },
412 : ),
413 : );
414 :
415 : // If there was any PathParamsPopError, we have some pathParamsPopErrors.values
416 : // and therefore should return this
417 4 : if (pathParamsPopErrors.values.isNotEmpty) {
418 : return pathParamsPopErrors;
419 : }
420 :
421 : // If none of the stackedRoutes popped, this did not pop, and there was no path parameters issue, return not found
422 : // This should never happen
423 0 : return ErrorNotFoundGetPathFromPopResult();
424 : }
425 : }
|