Line data Source code
1 : import 'package:flutter/widgets.dart';
2 : import 'package:theseus_navigator/theseus_navigator.dart';
3 :
4 : import 'destination_parser.dart';
5 : import 'navigation_controller.dart';
6 : import 'redirection.dart';
7 :
8 : /// A class that contains all required information about navigation target.
9 : ///
10 : /// The destination is identified by its [path]. Optionally, [parameters] can be provided.
11 : /// Either content [builder] or nested [navigator] must be provided for the destination.
12 : ///
13 : /// The navigator uses a destination's [configuration] to apply a certain logic of
14 : /// updating the navigation stack and transition animations.
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 : /// - [DestinationConfiguration]
26 : /// - [DestinationParameters]
27 : /// - [DestinationParser]
28 : /// - [Redirection]
29 : ///
30 : class Destination<T extends DestinationParameters> {
31 : /// Creates a destination.
32 : ///
33 9 : Destination({
34 : required this.path,
35 : this.builder,
36 : DestinationConfiguration? configuration,
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 9 : }) : assert(navigator != null || builder != null,
45 : 'Either "builder" or "navigator" must be specified.'),
46 : assert(
47 9 : (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 18 : ((T == DestinationParameters) &&
52 9 : (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 18 : this.configuration = configuration ?? DestinationConfiguration.material();
57 : }
58 :
59 : /// Creates a destination that provides a navigator with nested destinations.
60 : ///
61 0 : Destination.intermediate({
62 : required this.path,
63 : required this.navigator,
64 : this.isHome = false,
65 : this.redirections = const <Redirection>[],
66 : this.tag,
67 : }) : builder = null,
68 0 : configuration = DestinationConfiguration.material(),
69 : parameters = null,
70 : parser = const DefaultDestinationParser(),
71 : upwardDestinationBuilder = null;
72 :
73 : /// Path identifies the destination.
74 : ///
75 : /// Usually it follows the common url pattern with optional parameters.
76 : /// Example: `/catalog/{id}`
77 : ///
78 : final String path;
79 :
80 : /// A content builder.
81 : ///
82 : /// Returns a widget (basically a screen) that should be rendered for this destination.
83 : ///
84 : final Widget Function(BuildContext context, T? parameters)? builder;
85 :
86 : /// Defines a way of how this destination will appear.
87 : ///
88 : late final DestinationConfiguration configuration;
89 :
90 : /// Whether the destination is the home destination.
91 : ///
92 : /// The home destination matches the '/' or empty path, beside of its specific [path].
93 : ///
94 : final bool isHome;
95 :
96 : /// A child navigator.
97 : ///
98 : /// Allows to implement nested navigation. When specified, the parent navigator
99 : /// uses this child navigator to build content for this destination.
100 : ///
101 : final NavigationController? navigator;
102 :
103 : /// Optional parameters, that are used to build content.
104 : ///
105 : final T? parameters;
106 :
107 : /// A destination parser.
108 : ///
109 : /// Used to parse the certain destination object from the URI string, based on
110 : /// the current destination, and to generate a URI string from the current destination.
111 : ///
112 : final DestinationParser parser;
113 :
114 : /// Destinations and conditions to redirect.
115 : ///
116 : /// When it is not empty, the navigator will check for each [Redirection] in the list,
117 : /// if this destination is allowed to navigate to.
118 : ///
119 : final List<Redirection> redirections;
120 :
121 : /// An optional label to identify a destination.
122 : ///
123 : /// It will be the same for all destinations of the kind, regardless actual
124 : /// values of destination parameters.
125 : ///
126 : final String? tag;
127 :
128 : /// Function that returns an underlay destination.
129 : ///
130 : /// A [NavigationController] uses this method to create the underlay destination for the
131 : /// current one, using its parameters.
132 : ///
133 : final Destination? Function(Destination<T> destination)?
134 : upwardDestinationBuilder;
135 :
136 : /// Indicates if the [upwardDestinationBuilder] is provided.
137 : ///
138 6 : bool get hasUpwardDestinationBuilder => upwardDestinationBuilder != null;
139 :
140 : /// Whether this destination is final, i.e. it builds a content
141 : ///
142 : /// Final destinations must have a [builder] function provided.
143 : /// Non-final destinations must have a [navigator], that manages its own destinations.
144 : ///
145 10 : bool get isFinalDestination => navigator == null;
146 :
147 : /// A full URI of the destination, with parameters placeholders replaced with
148 : /// actual parameter values.
149 : ///
150 24 : String get uri => parser.uri(this);
151 :
152 : /// Check if the destination matches the provided URI string
153 : ///
154 21 : bool isMatch(String uri) => parser.isMatch(uri, this);
155 :
156 : /// Parses the destination from the provided URI string.
157 : ///
158 : /// Returns a copy of the current destination with updated parameters, parsed
159 : /// from the URI.
160 : /// If the URI doesn't match this destination, throws an [DestinationNotMatchException].
161 : ///
162 4 : Future<Destination<T>> parse(String uri) =>
163 8 : parser.parseParameters(uri, this) as Future<Destination<T>>;
164 :
165 : /// Return a widget that display destination's content.
166 : ///
167 : /// If the destination is final, then [builder] is called to build a content.
168 : /// Otherwise [navigator.build] is called to build nested navigator's content.
169 : ///
170 4 : Widget build(BuildContext context) => isFinalDestination
171 6 : ? builder!(context, parameters)
172 0 : : navigator!.build(context);
173 :
174 : /// Return a destination that should be displayed on reverse navigation.
175 : ///
176 12 : Destination? get upwardDestination => upwardDestinationBuilder?.call(this);
177 :
178 : /// Returns a copy of this destination with a different configuration.
179 : ///
180 3 : Destination<T> withConfiguration(DestinationConfiguration configuration) =>
181 3 : copyWith(
182 : configuration: configuration,
183 : );
184 :
185 : /// Returns a copy of this destination with different parameters.
186 : ///
187 : /// For typed parameters ensures that raw parameter values in [DestinationParameters.map] are valid.
188 : ///
189 7 : Destination<T> withParameters(T parameters) {
190 14 : final rawParameters = parser.toMap(parameters);
191 7 : return copyWith(
192 : parameters: parameters
193 14 : ..map.clear()
194 14 : ..map.addAll(rawParameters),
195 : );
196 : }
197 :
198 : /// Creates a copy of this destination with the given fields replaced
199 : /// with the new values.
200 : ///
201 7 : Destination<T> copyWith({
202 : DestinationConfiguration? configuration,
203 : T? parameters,
204 : }) =>
205 7 : Destination<T>(
206 7 : path: path,
207 7 : builder: builder,
208 7 : navigator: navigator,
209 7 : configuration: configuration ?? this.configuration,
210 3 : parameters: parameters ?? this.parameters,
211 7 : parser: parser,
212 7 : tag: tag,
213 7 : upwardDestinationBuilder: upwardDestinationBuilder,
214 : );
215 :
216 : /// Destinations are equal when their URI string are equal.
217 : ///
218 8 : @override
219 : bool operator ==(Object other) =>
220 : identical(this, other) ||
221 8 : other is Destination &&
222 24 : runtimeType == other.runtimeType &&
223 24 : uri == other.uri;
224 :
225 5 : @override
226 10 : int get hashCode => uri.hashCode;
227 :
228 5 : @override
229 5 : String toString() => uri;
230 : }
231 :
232 : /// Encapsulates the configuration attributes which are used for navigating to
233 : /// the destination.
234 : ///
235 : /// There are convenient factory constructors of commonly used configurations.
236 : /// [defaultMaterial] - pushes the destination to the navigation stack with standard material animations.
237 : /// [quiet] - replace the previous destination with the current one without animations.
238 : ///
239 : /// See also:
240 : /// - [DestinationAction]
241 : /// - [DestinationTransition]
242 : ///
243 : class DestinationConfiguration {
244 : /// Creates configuration of a destination.
245 : ///
246 9 : const DestinationConfiguration({
247 : required this.action,
248 : required this.transition,
249 : this.reset = false,
250 : this.transitionBuilder,
251 : }) : assert(
252 18 : (transition == DestinationTransition.custom &&
253 : transitionBuilder != null) ||
254 9 : (transition != DestinationTransition.custom),
255 : 'You have to provide "transitionBuilder" for "custom" transition.');
256 :
257 : /// Creates a configuration that pushes a destination to the top of navigation
258 : /// stack with a standard Material animations.
259 : ///
260 : const factory DestinationConfiguration.material() =
261 : _DefaultDestinationConfiguration;
262 :
263 : /// Creates a configuration that replaces the current destination with a new one
264 : /// with no animations.
265 : ///
266 : const factory DestinationConfiguration.quiet() =
267 : _QuietDestinationConfiguration;
268 :
269 : /// How the destination will update the navigation stack.
270 : ///
271 : /// See also:
272 : /// - [DestinationAction]
273 : ///
274 : final DestinationAction action;
275 :
276 : /// Visual effects that would be applied on updating the stack with the destination.
277 : ///
278 : /// See also:
279 : /// - [DestinationTransition]
280 : ///
281 : final DestinationTransition transition;
282 :
283 : /// Whether the stack would be cleared before adding the destination.
284 : ///
285 : final bool reset;
286 :
287 : /// Function that build custom destination transitions.
288 : ///
289 : /// It is required when the [transition] value is [DestinationTransition.custom].
290 : ///
291 : /// See also
292 : /// - [RouteTransitionBuilder]
293 : ///
294 : final RouteTransitionsBuilder? transitionBuilder;
295 :
296 : /// Creates a copy of this configuration with the given fields replaced
297 : /// with the new values.
298 : ///
299 3 : DestinationConfiguration copyWith({
300 : // TODO: Add other properties
301 : bool? reset,
302 : }) =>
303 3 : DestinationConfiguration(
304 3 : action: action,
305 3 : transition: transition,
306 0 : reset: reset ?? this.reset,
307 3 : transitionBuilder: transitionBuilder,
308 : );
309 : }
310 :
311 : class _DefaultDestinationConfiguration extends DestinationConfiguration {
312 9 : const _DefaultDestinationConfiguration()
313 9 : : super(
314 : action: DestinationAction.push,
315 : transition: DestinationTransition.material,
316 : );
317 : }
318 :
319 : class _QuietDestinationConfiguration extends DestinationConfiguration {
320 2 : const _QuietDestinationConfiguration()
321 2 : : super(
322 : action: DestinationAction.replace,
323 : transition: DestinationTransition.none,
324 : );
325 : }
326 :
327 : /// An action that is used to update the navigation stack with the destination.
328 : ///
329 9 : enum DestinationAction {
330 : /// The destination will be added to the navigation stack.
331 : /// On navigation back, the destination will be removed from the stack
332 : /// and previous destination will be restored.
333 : ///
334 : push,
335 :
336 : /// The previous destination will be removed from the navigation stack,
337 : /// and the current destination will be added.
338 : /// This means that user will not be able to return to previous destination
339 : /// by back navigation.
340 : ///
341 : replace,
342 : }
343 :
344 : /// Defines transition animations from the previous destination to the current one.
345 : ///
346 9 : enum DestinationTransition {
347 : /// Standard Material animations.
348 : ///
349 : material,
350 :
351 : /// Custom animations.
352 : ///
353 : custom,
354 :
355 : /// No animations.
356 : ///
357 : none,
358 : }
359 :
360 : /// Base destination parameters.
361 : ///
362 : /// Extend this abstract class to define your custom parameters class.
363 : /// Use [Destination<YourCustomDestinationParameters>()] to make a destination
364 : /// aware of your custom parameters.
365 : ///
366 : /// For custom parameters you also must implement [YouCustomDestinationParser<YourCustomDestinationParameters>]
367 : /// with [toDestinationParameters()] ans [toMap()] methods, like this:
368 : /// ```
369 : /// class YourCustomDestinationParser
370 : /// extends DestinationParser<YourCustomDestinationParameters> {
371 : /// const YourCustomDestinationParser() : super();
372 : ///
373 : /// @override
374 : /// YourCustomDestinationParameters toDestinationParameters(
375 : /// Map<String, String> map) {
376 : /// ...
377 : /// }
378 : ///
379 : /// @override
380 : /// Map<String, String> toMap(YourCustomDestinationParameters parameters) {
381 : /// ...
382 : /// }
383 : /// }
384 : /// ```
385 : ///
386 : /// See also:
387 : /// - [DestinationParser]
388 : ///
389 : class DestinationParameters {
390 : /// Creates a [DestinationParameters] instance.
391 : ///
392 7 : DestinationParameters([Map<String, String>? map])
393 4 : : map = map ?? <String, String>{};
394 :
395 : /// Contains parameter values parsed from the destination's URI.
396 : ///
397 : /// The parameter name is a [MapEntry.key], and the value is [MapEntry.value].
398 : ///
399 : late final Map<String, String> map;
400 : }
|