typed_deep_links 0.1.0
typed_deep_links: ^0.1.0 copied to clipboard
Type-safe, router-agnostic deep links with generated URI parsing and building.
typed_deep_links #
Type-safe deep links without coupling domain code to a router.
Declare URI shape beside immutable data classes. build_runner generates
parsing, validation, exhaustive-pattern-friendly values, and reverse URL
building. Use generated values with GoRouter, AutoRoute, Navigator, server
handlers, or any custom router.
import 'package:typed_deep_links/typed_deep_links.dart';
part 'links.g.dart';
@DeepLink('/orders/')
final class OrderLink {
const OrderLink({required this.id, this.source});
final int id;
@QueryParameter()
final String? source;
}
Generate code:
dart run build_runner build --delete-conflicting-outputs
Parse and pattern match:
final link = DeepLinks.parse(Uri.parse('/orders/42?source=email'));
switch (link) {
case OrderLink(:final id, :final source):
openOrder(id, source: source);
}
Build URLs:
const OrderLink(id: 42, source: 'push').toUri();
// /orders/42?source=push
DeepLinks.toUri(const OrderLink(id: 42));
// /orders/42
Route syntax #
Use explicit placeholders when useful:
@DeepLink('/shops/:shopId/orders/:orderId')
final class ShopOrderLink {
const ShopOrderLink({required this.shopId, required this.orderId});
final int shopId;
final int orderId;
}
A path ending in / appends remaining unannotated constructor fields as path
segments. This is why @DeepLink('/orders/') maps id to /orders/42.
Parameter annotations control other placements:
@DeepLink('/search')
final class SearchLink {
const SearchLink({this.query, this.section});
@QueryParameter('q')
final String? query;
@FragmentParameter()
final String? section;
}
@PathParameter('name'): maps a field to a placeholder or appended segment.@QueryParameter('key'): maps a field to one query value.@FragmentParameter(): maps one field to the URI fragment.schemeandhost: constrain absolute app/universal links.caseSensitive: false: allows case-insensitive static path segments.
Supported field types: String, int, double, num, bool, DateTime,
Uri, and enums. Nullable query/fragment fields are optional. Path fields must
be non-nullable. Boolean parsing accepts true, false, 1, and 0.
Failure handling #
DeepLinks.parse throws:
DeepLinkNotFoundExceptionwhen no route matches.DeepLinkFormatExceptionwhen a route matches but a required or typed value is invalid.
Alternatives:
final value = DeepLinks.tryParse(uri); // null only when no route matches
switch (DeepLinks.parseResult(uri)) {
case DeepLinkSuccess(:final value):
handle(value);
case DeepLinkFailure(:final error):
log(error);
}
Router integration #
No router dependency is required. Keep a small adapter at app boundary:
// GoRouter redirect/route builder, AutoRoute guard, or Navigator handler:
Object? parseLocation(String location) =>
DeepLinks.tryParse(Uri.parse(location));
String restoreLocation(Object link) => DeepLinks.toUri(link).toString();
Absolute links work with the same API:
@DeepLink('/orders/:id', scheme: 'myapp', host: 'open')
final class AppOrderLink { /* ... */ }
For web origins or API bases, resolve relative routes while building:
link.toUri(baseUri: Uri.parse('https://example.com/app/'));
Organization and generated registry #
One DeepLinks registry is generated per Dart library. Put related annotated
classes in one library, using part files if desired. This avoids global state,
runtime reflection, and router-specific registration.
Generated route order prefers more static segments before dynamic routes, so
/orders/new wins over /orders/:id. Conflicting route shapes fail generation.
Package setup #
Add runtime and generator dependency:
dart pub add typed_deep_links
dart pub add --dev build_runner
Flutter projects use the same commands with dart run build_runner.
See example/example.dart for a complete example.
License #
MIT.