Repository navigation
Expand file tree
/
Copy pathdeep_links.dart
More file actions
147 lines (133 loc) · 5.33 KB
/
Copy pathdeep_links.dart
File metadata and controls
147 lines (133 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import 'package:example/src/routes.dart';
import 'package:flutter/widgets.dart';
import 'package:squid/squid.dart';
/// Where a deep link leads: the tab to select and the stack of that tab.
typedef DeepLink = ({AppTab tab, NavigationStack stack});
/// The link the application has been opened with, or `null`.
///
/// The engine hands it to the framework as the initial route name:
/// * on the web it is the path of the address bar — `/shop/product/3`;
/// * on Android and iOS it is the path of the link that launched the
/// application, once deep linking is enabled for the platform
/// (`flutter_deeplinking_enabled` in `AndroidManifest.xml` / `Info.plist`);
/// * otherwise it is `/`.
///
/// The links that arrive while the application is already running are
/// delivered to [WidgetsBindingObserver.didPushRouteInformation] instead.
Uri? initialDeepLink() {
final name = WidgetsBinding.instance.platformDispatcher.defaultRouteName;
if (name.isEmpty || name == Navigator.defaultRouteName) return null;
return Uri.tryParse(name);
}
/// Squid does not dictate the format of the links: a link is converted to a
/// stack by a plain function. Below are three ways to write it, pick the one
/// that matches the shape of your links.
/* #region 1. A switch over the path segments */
/// A `switch` with patterns over [Uri.pathSegments].
///
/// The most readable option while the links are few: every shape of a link
/// is a single line, the arguments are extracted and validated in place.
DeepLink? parseDeepLinkWithSwitch(Uri uri) => switch (uri.pathSegments) {
[] || ['shop'] => (tab: AppTab.shop, stack: <NavigationRoute>[]),
['shop', 'filters'] => (
tab: AppTab.shop,
stack: <NavigationRoute>[const FiltersRoute()],
),
['shop', 'product', final id] when int.tryParse(id) != null => (
tab: AppTab.shop,
stack: <NavigationRoute>[ProductRoute(int.parse(id))],
),
['cart'] => (tab: AppTab.cart, stack: <NavigationRoute>[]),
['account'] => (tab: AppTab.account, stack: <NavigationRoute>[]),
['account', 'settings'] => (
tab: AppTab.account,
stack: <NavigationRoute>[Routes.settings],
),
_ => null,
};
/* #endregion */
/* #region 2. A table of regular expressions */
/// A table of `(pattern, builder)` pairs, the first match wins.
///
/// Handy when the links are many, come from the backend or from marketing,
/// and are easier to maintain as data than as code.
final List<(RegExp, DeepLink Function(RegExpMatch match))> deepLinkTable =
<(RegExp, DeepLink Function(RegExpMatch match))>[
(
RegExp(r'^/shop/product/(?<id>\d+)/?$'),
(match) => (
tab: AppTab.shop,
stack: <NavigationRoute>[
ProductRoute(int.parse(match.namedGroup('id')!)),
],
),
),
(
RegExp(r'^/shop/filters/?$'),
(_) =>
(tab: AppTab.shop, stack: <NavigationRoute>[const FiltersRoute()]),
),
(
RegExp(r'^/account/settings/?$'),
(_) => (tab: AppTab.account, stack: <NavigationRoute>[Routes.settings]),
),
(
RegExp(r'^/(?<tab>shop|cart|account)?/?$'),
(match) => (
tab: AppTab.values.byName(match.namedGroup('tab') ?? 'shop'),
stack: <NavigationRoute>[],
),
),
];
/// Resolves [uri] with the [deepLinkTable].
DeepLink? parseDeepLinkWithTable(Uri uri) {
final path = uri.path.isEmpty ? '/' : uri.path;
for (final (pattern, build) in deepLinkTable) {
final match = pattern.firstMatch(path);
if (match != null) return build(match);
}
return null;
}
/* #endregion */
/* #region 3. Every segment is a route */
/// Every segment of the path becomes a route of the stack:
/// `/shop/product-3/product-4/filters` opens the product 3, the product 4
/// above it and the filters on top.
///
/// The most flexible option: any combination of the screens can be linked
/// without declaring it in advance, and the address mirrors the stack, which
/// is exactly what the web expects. The guards still have the last word, so
/// a nonsensical combination is fixed by them the same way as a mistake in
/// the code would be.
DeepLink? parseDeepLinkBySegments(Uri uri) {
final segments = uri.pathSegments.where((segment) => segment.isNotEmpty);
if (segments.isEmpty) return (tab: AppTab.shop, stack: <NavigationRoute>[]);
final tab = AppTab.values.asNameMap()[segments.first];
if (tab == null) return null;
final stack = <NavigationRoute>[];
for (final segment in segments.skip(1)) {
final route = switch (segment.split('-')) {
['product', final id] when int.tryParse(id) != null => ProductRoute(
int.parse(id),
),
['filters'] => const FiltersRoute(),
['settings'] => Routes.settings,
// An unknown segment is skipped instead of failing the whole link.
_ => null,
};
if (route != null) stack.add(route);
}
return (tab: tab, stack: stack);
}
/* #endregion */
/// Opens [link] in [tabs]: the routes of the link are put on top of the root
/// of the tab, and the tab is selected.
///
/// The stack is replaced as a whole, so the guards validate the final
/// result only once: a signed out user still lands on the sign in screen.
void openDeepLink(NavigationTabsController<AppTab> tabs, DeepLink link) {
tabs[link.tab].change(
(stack) => <NavigationRoute>[stack.first, ...link.stack],
);
tabs.select(link.tab, popToRoot: false);
}