Line data Source code
1 : import 'package:flutter/widgets.dart';
2 :
3 : import 'destination_parser.dart';
4 : import 'navigation_controller.dart';
5 : import 'redirection.dart';
6 :
7 : /// A class that contains all required information about navigation target.
8 : ///
9 : /// The destination is identified by its [path]. Optionally, [parameters] can be provided.
10 : /// Either content [builder] or nested [navigator] must be provided for the destination.
11 : ///
12 : /// The navigator uses a destination's [settings] to determine a way of changing
13 : /// the navigation stack, apply transition animations and other aspects of updating
14 : /// the navigation state.
15 : ///
16 : /// The [parser] is used to parse destination from the URI and generate a URI string
17 : /// for the destination.
18 : ///
19 : /// Optional [upwardDestinationBuilder] builder function can be used to implement custom
20 : /// logic of upward navigation from the current destination.
21 : ///
22 : /// If [redirections] are specified, they will be applied on navigation to this destination.
23 : ///
24 : /// See also:
25 : /// - [DestinationSettings]
26 : /// - [DestinationParameters]
27 : /// - [DestinationParser]
28 : /// - [Redirection]
29 : ///
30 : class Destination<T extends DestinationParameters> {
31 : /// Creates a destination.
32 : ///
33 10 : Destination({
34 : required this.path,
35 : this.builder,
36 : this.isHome = false,
37 : this.navigator,
38 : this.parameters,
39 : this.parser = const DefaultDestinationParser(),
40 : this.redirections = const <Redirection>[],
41 : DestinationSettings? settings,
42 : this.tag,
43 : this.upwardDestinationBuilder,
44 10 : }) : assert(navigator != null || builder != null,
45 : 'Either "builder" or "navigator" must be specified.'),
46 : assert(
47 10 : (navigator != null && builder == null) ||
48 : (builder != null && navigator == null),
49 : 'If the "navigator" is provided, the "builder" must be null, or vice versa.'),
50 : assert(
51 20 : ((T == DestinationParameters) &&
52 10 : (parser is DefaultDestinationParser)) ||
53 6 : ((T != DestinationParameters) &&
54 6 : parser is! DefaultDestinationParser),
55 0 : 'Custom "parser" must be provided when using the parameters of type $T, but ${parser.runtimeType} was provided.') {
56 20 : this.settings = settings ?? DestinationSettings.material();
57 10 : _transitBuilder = null;
58 : }
59 :
60 : /// Creates a destination that provides a navigator with nested destinations.
61 : ///
62 : /// An optional [builder] parameter is basically the same like normal [Destination.builder],
63 : /// but has additional [child] parameter, which contains the nested content that built by [navigator].
64 : /// The implementation of [builder] function must include this child widget sub-tree
65 : /// in the result for correct displaying the nested content.
66 : ///
67 2 : Destination.transit({
68 : required this.path,
69 : required this.navigator,
70 : Widget Function(BuildContext context, T? parameters, Widget child)? builder,
71 : this.isHome = false,
72 : this.redirections = const <Redirection>[],
73 : this.tag,
74 : }) : builder = null,
75 : parameters = null,
76 : parser = const DefaultDestinationParser(),
77 2 : settings = DestinationSettings.material(),
78 : upwardDestinationBuilder = null,
79 : _transitBuilder = builder;
80 :
81 : /// Path identifies the destination.
82 : ///
83 : /// Usually it follows the common url pattern with optional parameters.
84 : /// Example: `/catalog/{id}`
85 : ///
86 : final String path;
87 :
88 : /// A content builder.
89 : ///
90 : /// Returns a widget (basically a screen) that should be rendered for this destination.
91 : ///
92 : final Widget Function(BuildContext context, T? parameters)? builder;
93 :
94 : /// Whether the destination is the home destination.
95 : ///
96 : /// The home destination matches the '/' or empty path, beside of its specific [path].
97 : ///
98 : final bool isHome;
99 :
100 : /// A child navigator.
101 : ///
102 : /// Allows to implement nested navigation. When specified, the parent navigator
103 : /// uses this child navigator to build content for this destination.
104 : ///
105 : final NavigationController? navigator;
106 :
107 : /// Parameters of the destination.
108 : ///
109 : /// If the type *T* is not specified for the destination, then default [DestinationParameters]
110 : /// type is used.
111 : ///
112 : final T? parameters;
113 :
114 : /// A destination parser.
115 : ///
116 : /// Used to parse the certain destination object from the URI string, based on
117 : /// the current destination, and to generate a URI string from the current destination.
118 : ///
119 : final DestinationParser parser;
120 :
121 : /// Destinations and conditions to redirect.
122 : ///
123 : /// When it is not empty, the navigator will check for each [Redirection] in the list,
124 : /// if this destination is allowed to navigate to.
125 : ///
126 : final List<Redirection> redirections;
127 :
128 : /// Defines a way of how this destination will appear.
129 : ///
130 : late final DestinationSettings settings;
131 :
132 : /// An optional label to identify a destination.
133 : ///
134 : /// It will be the same for all destinations of the kind, regardless actual
135 : /// values of destination parameters.
136 : ///
137 : final String? tag;
138 :
139 : /// Function that returns an underlay destination.
140 : ///
141 : /// A [NavigationController] uses this method to create the underlay destination for the
142 : /// current one, using its parameters.
143 : ///
144 : final Future<Destination?> Function(Destination<T> destination)?
145 : upwardDestinationBuilder;
146 :
147 : late final Widget Function(BuildContext context, T? parameters, Widget child)?
148 : _transitBuilder;
149 :
150 : /// Whether this destination is final, i.e. it builds a content
151 : ///
152 : /// Final destinations must have a [builder] function provided.
153 : /// Non-final destinations must have a [navigator], that manages its own destinations.
154 : ///
155 12 : bool get isFinalDestination => navigator == null;
156 :
157 : /// Return a destination that should be displayed on reverse navigation.
158 : ///
159 5 : Future<Destination?> get upwardDestination async =>
160 8 : upwardDestinationBuilder?.call(this);
161 :
162 : /// A full URI of the destination, with parameters placeholders replaced with
163 : /// actual parameter values.
164 : ///
165 27 : String get uri => parser.uri(this);
166 :
167 : /// Return a widget that display destination's content.
168 : ///
169 : /// If the destination is final, then [builder] is called to build the content.
170 : ///
171 : /// Otherwise [navigator.build] is called to build nested navigator's content.
172 : /// In case the destination was created by [Destination.transit] constructor,
173 : /// and [builder] parameter was specified, the nested content is also wrapped in
174 : /// the widget sub-tree returned by that builder.
175 : ///
176 3 : Widget build(BuildContext context) {
177 3 : if (isFinalDestination) {
178 9 : return builder!(context, parameters);
179 : } else {
180 4 : final nestedContent = navigator!.build(context);
181 2 : if (_transitBuilder != null) {
182 6 : return _transitBuilder!(context, parameters, nestedContent);
183 : } else {
184 : return nestedContent;
185 : }
186 : }
187 : }
188 :
189 : /// Check if the destination matches the provided URI string
190 : ///
191 21 : bool isMatch(String uri) => parser.isMatch(uri, this);
192 :
193 : /// Parses the destination from the provided URI string.
194 : ///
195 : /// Returns a copy of the current destination with updated parameters, parsed
196 : /// from the URI.
197 : /// If the URI doesn't match this destination, throws an [DestinationNotMatchException].
198 : ///
199 5 : Future<Destination<T>> parse(String uri) =>
200 10 : parser.parseParameters(uri, this) as Future<Destination<T>>;
201 :
202 : /// Returns a copy of this destination with a different settings.
203 : ///
204 10 : Destination<T> withSettings(DestinationSettings settings) => copyWith(
205 : settings: settings,
206 : );
207 :
208 : /// Returns a copy of this destination with different parameters.
209 : ///
210 : /// For typed parameters ensures that raw parameter values in [DestinationParameters.map]
211 : /// are updated as well.
212 : ///
213 8 : Destination<T> withParameters(T parameters) {
214 16 : final rawParameters = parser.parametersToMap(parameters);
215 8 : return copyWith(
216 16 : parameters: parameters..map.addAll(rawParameters),
217 : );
218 : }
219 :
220 : /// Creates a copy of this destination with the given fields replaced
221 : /// with the new values.
222 : ///
223 8 : Destination<T> copyWith({
224 : T? parameters,
225 : DestinationSettings? settings,
226 : Future<Destination?> Function(Destination<T> destination)?
227 : upwardDestinationBuilder,
228 : }) =>
229 8 : Destination<T>(
230 8 : path: path,
231 8 : builder: builder,
232 8 : navigator: navigator,
233 5 : parameters: parameters ?? this.parameters,
234 8 : parser: parser,
235 8 : redirections: redirections,
236 8 : settings: settings ?? this.settings,
237 8 : tag: tag,
238 : upwardDestinationBuilder:
239 8 : upwardDestinationBuilder ?? this.upwardDestinationBuilder,
240 : );
241 :
242 : /// Destinations are equal when their URI string are equal.
243 : ///
244 8 : @override
245 : bool operator ==(Object other) =>
246 : identical(this, other) ||
247 8 : other is Destination &&
248 24 : runtimeType == other.runtimeType &&
249 24 : uri == other.uri;
250 :
251 5 : @override
252 10 : int get hashCode => uri.hashCode;
253 :
254 6 : @override
255 6 : String toString() => uri;
256 : }
257 :
258 : /// Encapsulates the settings attributes which are applied when the navigation state
259 : /// is updated with the the destination.
260 : ///
261 : /// There are convenient factory constructors for commonly used settings.
262 : /// [material] - pushes the destination to the navigation stack with standard material animations.
263 : /// [dialog] - display the destination like a dialog
264 : /// [quiet] - replace the previous destination with the current one without animations.
265 : ///
266 : /// See also:
267 : /// - [DestinationAction]
268 : /// - [DestinationTransition]
269 : ///
270 : class DestinationSettings {
271 : /// Creates an instance of [DestinationSettings].
272 : ///
273 10 : const DestinationSettings({
274 : required this.action,
275 : required this.transition,
276 : this.redirectedFrom,
277 : this.reset = false,
278 : this.transitionBuilder,
279 : this.updateHistory = true,
280 : }) : assert(
281 20 : (transition == DestinationTransition.custom &&
282 : transitionBuilder != null) ||
283 10 : (transition != DestinationTransition.custom),
284 : 'You have to provide "transitionBuilder" for "custom" transition.');
285 :
286 : /// Creates a settings to push a destination to the top of navigation
287 : /// stack with a standard Material animations.
288 : ///
289 : const factory DestinationSettings.material() = _DefaultDestinationSettings;
290 :
291 : /// Creates a settings to displays a destination as a modal dialog.
292 : ///
293 : const factory DestinationSettings.dialog() = _DialogDestinationSettings;
294 :
295 : /// Creates a settings to replaces the current destination with a new one
296 : /// with no animations.
297 : ///
298 : const factory DestinationSettings.quiet() = _QuietDestinationSettings;
299 :
300 : /// How the destination will update the navigation stack.
301 : ///
302 : /// See also:
303 : /// - [DestinationAction]
304 : ///
305 : final DestinationAction action;
306 :
307 : /// Visual effects that would be applied on updating the stack with the destination.
308 : ///
309 : /// See also:
310 : /// - [DestinationTransition]
311 : ///
312 : final DestinationTransition transition;
313 :
314 : /// In case of redirection, contains a destination from which the redirection
315 : /// was performed.
316 : ///
317 : final Destination? redirectedFrom;
318 :
319 : /// Whether the stack would be cleared before adding the destination.
320 : ///
321 : final bool reset;
322 :
323 : /// Function that build custom destination transitions.
324 : ///
325 : /// It is required when the [transition] value is [DestinationTransition.custom].
326 : ///
327 : /// See also
328 : /// - [RouteTransitionBuilder]
329 : ///
330 : final RouteTransitionsBuilder? transitionBuilder;
331 :
332 : /// Controls if the destination will be added to the navigation history.
333 : ///
334 : /// Currently it only affects to web applications. When set to *true*, which is
335 : /// default, the url in the web browser address field will be updated with the [Destination.uri].
336 : ///
337 : final bool updateHistory;
338 :
339 : /// Creates a copy of this settings with the given fields replaced
340 : /// with the new values.
341 : ///
342 5 : DestinationSettings copyWith({
343 : // TODO: Add other properties
344 : DestinationAction? action,
345 : Destination? redirectedFrom,
346 : bool? reset,
347 : bool? updateHistory,
348 : }) =>
349 5 : DestinationSettings(
350 4 : action: action ?? this.action,
351 5 : transition: transition,
352 5 : redirectedFrom: redirectedFrom ?? this.redirectedFrom,
353 4 : reset: reset ?? this.reset,
354 5 : transitionBuilder: transitionBuilder,
355 4 : updateHistory: updateHistory ?? this.updateHistory,
356 : );
357 : }
358 :
359 : class _DefaultDestinationSettings extends DestinationSettings {
360 10 : const _DefaultDestinationSettings()
361 10 : : super(
362 : action: DestinationAction.push,
363 : transition: DestinationTransition.material,
364 : );
365 : }
366 :
367 : class _DialogDestinationSettings extends DestinationSettings {
368 2 : const _DialogDestinationSettings()
369 2 : : super(
370 : action: DestinationAction.push,
371 : transition: DestinationTransition.materialDialog,
372 : );
373 : }
374 :
375 : class _QuietDestinationSettings extends DestinationSettings {
376 2 : const _QuietDestinationSettings()
377 2 : : super(
378 : action: DestinationAction.replace,
379 : transition: DestinationTransition.none,
380 : );
381 : }
382 :
383 : /// An action that is used to update the navigation stack with the destination.
384 : ///
385 11 : enum DestinationAction {
386 : /// The destination will be added to the navigation stack.
387 : /// On navigation back, the destination will be removed from the stack
388 : /// and previous destination will be restored.
389 : ///
390 : push,
391 :
392 : /// The previous destination will be removed from the navigation stack,
393 : /// and the current destination will be added.
394 : /// This means that user will not be able to return to previous destination
395 : /// by back navigation.
396 : ///
397 : replace,
398 : }
399 :
400 : /// Defines transition animations from the previous destination to the current one.
401 : ///
402 11 : enum DestinationTransition {
403 : /// Standard Material animations.
404 : ///
405 : material,
406 :
407 : /// Destination appears as a dialog with Material transitions and modal barrier.
408 : ///
409 : materialDialog,
410 :
411 : /// Custom animations.
412 : ///
413 : custom,
414 :
415 : /// No animations.
416 : ///
417 : none,
418 : }
419 :
420 : /// Base destination parameters.
421 : ///
422 : /// Extend this class to define your custom parameters class.
423 : /// Use [Destination<YourCustomDestinationParameters>()] to make a destination
424 : /// aware of your custom parameters.
425 : ///
426 : /// For custom parameters you also must implement [YouCustomDestinationParser<YourCustomDestinationParameters>]
427 : /// with [toDestinationParameters()] ans [toMap()] methods, like this:
428 : /// ``` dart
429 : /// class YourCustomDestinationParser
430 : /// extends DestinationParser<YourCustomDestinationParameters> {
431 : /// const YourCustomDestinationParser() : super();
432 : ///
433 : /// @override
434 : /// YourCustomDestinationParameters toDestinationParameters(
435 : /// Map<String, String> map) {
436 : /// //...
437 : /// }
438 : ///
439 : /// @override
440 : /// Map<String, String> toMap(YourCustomDestinationParameters parameters) {
441 : /// //...
442 : /// }
443 : /// }
444 : /// ```
445 : ///
446 : /// See also:
447 : /// - [DestinationParser]
448 : ///
449 : class DestinationParameters {
450 : /// Creates a [DestinationParameters] instance.
451 : ///
452 8 : DestinationParameters([Map<String, String>? map])
453 5 : : map = map ?? <String, String>{};
454 :
455 : /// Reserved query parameter name.
456 : ///
457 : /// It is used for automatic persisting of navigation state.
458 : /// Do not use this name for your custom parameters.
459 : ///
460 : static const String stateParameterName = 'state';
461 :
462 : static const _reservedParameterNames = <String>{
463 : stateParameterName,
464 : };
465 :
466 : /// Contains parameter values parsed from the destination's URI.
467 : ///
468 : /// The parameter name is a [MapEntry.key], and the value is [MapEntry.value].
469 : ///
470 : late final Map<String, String> map;
471 :
472 : /// Check if a provided parameter name is reserved
473 : ///
474 : /// This function is used by [DestinationParser] to synchronize internal raw parameter
475 : /// values with parsed parameter object properties and build destination URI.
476 : ///
477 6 : static bool isReservedParameter(String parameterName) =>
478 6 : _reservedParameterNames.contains(parameterName);
479 : }
|