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 : DestinationSettings? settings,
37 : this.isHome = false,
38 : this.navigator,
39 : this.parameters,
40 : this.parser = const DefaultDestinationParser(),
41 : this.redirections = const <Redirection>[],
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 5 : ((T != DestinationParameters) &&
54 5 : 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 2 : settings = DestinationSettings.material(),
76 : parameters = null,
77 : parser = const DefaultDestinationParser(),
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 : /// Defines a way of how this destination will appear.
95 : ///
96 : late final DestinationSettings settings;
97 :
98 : /// Whether the destination is the home destination.
99 : ///
100 : /// The home destination matches the '/' or empty path, beside of its specific [path].
101 : ///
102 : final bool isHome;
103 :
104 : /// A child navigator.
105 : ///
106 : /// Allows to implement nested navigation. When specified, the parent navigator
107 : /// uses this child navigator to build content for this destination.
108 : ///
109 : final NavigationController? navigator;
110 :
111 : /// Optional parameters, that are used to build content.
112 : ///
113 : final T? parameters;
114 :
115 : /// A destination parser.
116 : ///
117 : /// Used to parse the certain destination object from the URI string, based on
118 : /// the current destination, and to generate a URI string from the current destination.
119 : ///
120 : final DestinationParser parser;
121 :
122 : /// Destinations and conditions to redirect.
123 : ///
124 : /// When it is not empty, the navigator will check for each [Redirection] in the list,
125 : /// if this destination is allowed to navigate to.
126 : ///
127 : final List<Redirection> redirections;
128 :
129 : /// An optional label to identify a destination.
130 : ///
131 : /// It will be the same for all destinations of the kind, regardless actual
132 : /// values of destination parameters.
133 : ///
134 : final String? tag;
135 :
136 : /// Function that returns an underlay destination.
137 : ///
138 : /// A [NavigationController] uses this method to create the underlay destination for the
139 : /// current one, using its parameters.
140 : ///
141 : final Destination? Function(Destination<T> destination)?
142 : upwardDestinationBuilder;
143 :
144 : late final Widget Function(BuildContext context, T? parameters, Widget child)?
145 : _transitBuilder;
146 :
147 : /// Indicates if the [upwardDestinationBuilder] is provided.
148 : ///
149 0 : bool get hasUpwardDestinationBuilder => upwardDestinationBuilder != null;
150 :
151 : /// Whether this destination is final, i.e. it builds a content
152 : ///
153 : /// Final destinations must have a [builder] function provided.
154 : /// Non-final destinations must have a [navigator], that manages its own destinations.
155 : ///
156 12 : bool get isFinalDestination => navigator == null;
157 :
158 : /// Return a destination that should be displayed on reverse navigation.
159 : ///
160 12 : Destination? get upwardDestination => 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 4 : Future<Destination<T>> parse(String uri) =>
200 8 : parser.parseParameters(uri, this) as Future<Destination<T>>;
201 :
202 : /// Returns a copy of this destination with a different settings.
203 : ///
204 4 : Destination<T> withSettings(DestinationSettings settings) =>
205 4 : copyWith(
206 : settings: settings,
207 : );
208 :
209 : /// Returns a copy of this destination with different parameters.
210 : ///
211 : /// For typed parameters ensures that raw parameter values in [DestinationParameters.map] are valid.
212 : ///
213 7 : Destination<T> withParameters(T parameters) {
214 14 : final rawParameters = parser.toMap(parameters);
215 7 : return copyWith(
216 : parameters: parameters
217 14 : ..map.clear()
218 14 : ..map.addAll(rawParameters),
219 : );
220 : }
221 :
222 : /// Creates a copy of this destination with the given fields replaced
223 : /// with the new values.
224 : ///
225 7 : Destination<T> copyWith({
226 : DestinationSettings? settings,
227 : T? parameters,
228 : }) =>
229 7 : Destination<T>(
230 7 : path: path,
231 7 : builder: builder,
232 7 : navigator: navigator,
233 7 : settings: settings ?? this.settings,
234 4 : parameters: parameters ?? this.parameters,
235 7 : parser: parser,
236 7 : redirections: redirections,
237 7 : tag: tag,
238 7 : upwardDestinationBuilder: upwardDestinationBuilder,
239 : );
240 :
241 : /// Destinations are equal when their URI string are equal.
242 : ///
243 8 : @override
244 : bool operator ==(Object other) =>
245 : identical(this, other) ||
246 8 : other is Destination &&
247 24 : runtimeType == other.runtimeType &&
248 24 : uri == other.uri;
249 :
250 5 : @override
251 10 : int get hashCode => uri.hashCode;
252 :
253 6 : @override
254 6 : String toString() => uri;
255 : }
256 :
257 : /// Encapsulates the settings attributes which are applied when the navigation state
258 : /// is updated with the the destination.
259 : ///
260 : /// There are convenient factory constructors for commonly used settings.
261 : /// [material] - pushes the destination to the navigation stack with standard material animations.
262 : /// [dialog] - display the destination like a dialog
263 : /// [quiet] - replace the previous destination with the current one without animations.
264 : ///
265 : /// See also:
266 : /// - [DestinationAction]
267 : /// - [DestinationTransition]
268 : ///
269 : class DestinationSettings {
270 : /// Creates an instance of [DestinationSettings].
271 : ///
272 10 : const DestinationSettings({
273 : required this.action,
274 : required this.transition,
275 : this.redirectedFrom,
276 : this.reset = false,
277 : this.transitionBuilder,
278 : }) : assert(
279 20 : (transition == DestinationTransition.custom &&
280 : transitionBuilder != null) ||
281 10 : (transition != DestinationTransition.custom),
282 : 'You have to provide "transitionBuilder" for "custom" transition.');
283 :
284 : /// Creates a settings to push a destination to the top of navigation
285 : /// stack with a standard Material animations.
286 : ///
287 : const factory DestinationSettings.material() =
288 : _DefaultDestinationSettings;
289 :
290 : /// Creates a settings to displays a destination as a modal dialog.
291 : ///
292 : const factory DestinationSettings.dialog() =
293 : _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() =
299 : _QuietDestinationSettings;
300 :
301 : /// How the destination will update the navigation stack.
302 : ///
303 : /// See also:
304 : /// - [DestinationAction]
305 : ///
306 : final DestinationAction action;
307 :
308 : /// Visual effects that would be applied on updating the stack with the destination.
309 : ///
310 : /// See also:
311 : /// - [DestinationTransition]
312 : ///
313 : final DestinationTransition transition;
314 :
315 : /// In case of redirection, contains a destination from which the redirection
316 : /// was performed.
317 : ///
318 : final Destination? redirectedFrom;
319 :
320 : /// Whether the stack would be cleared before adding the destination.
321 : ///
322 : final bool reset;
323 :
324 : /// Function that build custom destination transitions.
325 : ///
326 : /// It is required when the [transition] value is [DestinationTransition.custom].
327 : ///
328 : /// See also
329 : /// - [RouteTransitionBuilder]
330 : ///
331 : final RouteTransitionsBuilder? transitionBuilder;
332 :
333 : /// Creates a copy of this settings with the given fields replaced
334 : /// with the new values.
335 : ///
336 4 : DestinationSettings copyWith({
337 : // TODO: Add other properties
338 : Destination? redirectedFrom,
339 : bool? reset,
340 : }) =>
341 4 : DestinationSettings(
342 4 : action: action,
343 4 : transition: transition,
344 3 : redirectedFrom: redirectedFrom ?? this.redirectedFrom,
345 2 : reset: reset ?? this.reset,
346 4 : transitionBuilder: transitionBuilder,
347 : );
348 : }
349 :
350 : class _DefaultDestinationSettings extends DestinationSettings {
351 10 : const _DefaultDestinationSettings()
352 10 : : super(
353 : action: DestinationAction.push,
354 : transition: DestinationTransition.material,
355 : );
356 : }
357 :
358 : class _DialogDestinationSettings extends DestinationSettings {
359 2 : const _DialogDestinationSettings()
360 2 : : super(
361 : action: DestinationAction.push,
362 : transition: DestinationTransition.materialDialog,
363 : );
364 : }
365 :
366 : class _QuietDestinationSettings extends DestinationSettings {
367 2 : const _QuietDestinationSettings()
368 2 : : super(
369 : action: DestinationAction.replace,
370 : transition: DestinationTransition.none,
371 : );
372 : }
373 :
374 : /// An action that is used to update the navigation stack with the destination.
375 : ///
376 10 : enum DestinationAction {
377 : /// The destination will be added to the navigation stack.
378 : /// On navigation back, the destination will be removed from the stack
379 : /// and previous destination will be restored.
380 : ///
381 : push,
382 :
383 : /// The previous destination will be removed from the navigation stack,
384 : /// and the current destination will be added.
385 : /// This means that user will not be able to return to previous destination
386 : /// by back navigation.
387 : ///
388 : replace,
389 : }
390 :
391 : /// Defines transition animations from the previous destination to the current one.
392 : ///
393 10 : enum DestinationTransition {
394 : /// Standard Material animations.
395 : ///
396 : material,
397 :
398 : /// Destination appears as a dialog with Material transitions and modal barrier.
399 : ///
400 : materialDialog,
401 :
402 : /// Custom animations.
403 : ///
404 : custom,
405 :
406 : /// No animations.
407 : ///
408 : none,
409 : }
410 :
411 : /// Base destination parameters.
412 : ///
413 : /// Extend this abstract class to define your custom parameters class.
414 : /// Use [Destination<YourCustomDestinationParameters>()] to make a destination
415 : /// aware of your custom parameters.
416 : ///
417 : /// For custom parameters you also must implement [YouCustomDestinationParser<YourCustomDestinationParameters>]
418 : /// with [toDestinationParameters()] ans [toMap()] methods, like this:
419 : /// ``` dart
420 : /// class YourCustomDestinationParser
421 : /// extends DestinationParser<YourCustomDestinationParameters> {
422 : /// const YourCustomDestinationParser() : super();
423 : ///
424 : /// @override
425 : /// YourCustomDestinationParameters toDestinationParameters(
426 : /// Map<String, String> map) {
427 : /// //...
428 : /// }
429 : ///
430 : /// @override
431 : /// Map<String, String> toMap(YourCustomDestinationParameters parameters) {
432 : /// //...
433 : /// }
434 : /// }
435 : /// ```
436 : ///
437 : /// See also:
438 : /// - [DestinationParser]
439 : ///
440 : class DestinationParameters {
441 : /// Creates a [DestinationParameters] instance.
442 : ///
443 7 : DestinationParameters([Map<String, String>? map])
444 4 : : map = map ?? <String, String>{};
445 :
446 : /// Contains parameter values parsed from the destination's URI.
447 : ///
448 : /// The parameter name is a [MapEntry.key], and the value is [MapEntry.value].
449 : ///
450 : late final Map<String, String> map;
451 : }
|