typed_deep_links 0.3.1
typed_deep_links: ^0.3.1 copied to clipboard
Type-safe, router-agnostic deep links with generated URI parsing and building.
typed_deep_links #
Router-neutral, type-safe deep links for Dart and Flutter. Declare routes on immutable models; generated code parses URIs, validates values, builds canonical URIs, and exposes a typed registry plus route manifest.
Install #
dart pub add typed_deep_links
dart pub add --dev typed_deep_links_generator build_runner
Flutter apps can optionally add the Router API bridge:
flutter pub add typed_deep_links_flutter
Quick start #
import 'package:typed_deep_links/typed_deep_links.dart';
part 'links.g.dart';
sealed class AppLink {}
@DeepLink('/orders/:id', aliases: ['/purchases/:id'])
final class OrderLink implements AppLink {
const OrderLink({required this.id, this.source});
@PathParameter.config(validation: DeepLinkValidation(min: 1))
final int id;
@QueryParameter('source')
final String? source;
}
@DeepLinkRegistry(
routes: [OrderLink],
name: 'AppLinks',
baseType: AppLink,
)
final class AppLinkRegistry {}
Generate once, or use watch while developing:
dart run build_runner build
final AppLink link = AppLinks.parse(
Uri.parse('/orders/42?source=email'),
);
switch (link) {
case OrderLink(:final id, :final source):
openOrder(id, source: source);
}
const OrderLink(id: 42).toUri(); // /orders/42
AppLinks.toUri(link);
If a library has routes but no @DeepLinkRegistry, a backward-compatible
DeepLinks registry returning Object is generated automatically.
Route features #
Paths and migrations #
@DeepLink(
'/products/:slug/:tab?',
aliases: ['/catalog/:slug/:tab?'],
strictQueryParameters: true,
)
:nameis a required path segment.:name?is a trailing optional segment; its field must be nullable or have a constructor default.*nameis a final catch-all segment backed byString,List<String>, orSet<String>.aliasesare parse-only migration paths.toUri()always emits the canonical path.- A path ending in
/appends remaining unannotated constructor fields, so@DeepLink('/orders/')still maps anidfield to/orders/42.
Static routes win over dynamic routes. Ambiguous route shapes fail during code generation instead of depending on declaration order.
Query values, fragments, and enums #
enum SortOrder {
@DeepLinkEnumValue('best-match')
relevance,
@DeepLinkEnumValue('newest')
recent,
}
@DeepLink('/search', strictQueryParameters: true)
final class SearchLink {
const SearchLink({this.query, this.tags = const [], this.sort, this.section});
@QueryParameter('q')
final String? query;
@QueryParameter()
final List<String> tags; // repeated as ?tags=a&tags=b
@QueryParameter()
final SortOrder? sort;
@FragmentParameter()
final String? section;
}
Supported built-in scalar types are String, int, double, num, bool,
DateTime, Uri, and enums. Query values also support List<T> and Set<T>.
Boolean decoding accepts true, false, 1, and 0.
Custom value objects #
final class ProductIdCodec implements DeepLinkValueCodec<ProductId> {
const ProductIdCodec();
@override
ProductId decode(String value) => ProductId(value);
@override
String encode(ProductId value) => value.value;
}
@PathParameter.config(codec: ProductIdCodec)
final ProductId id;
A codec must implement DeepLinkValueCodec<T> and have a const unnamed,
zero-argument constructor.
Validation #
Use DeepLinkValidation on path, query, or fragment annotations:
@QueryParameter.config(
name: 'coupon',
validation: DeepLinkValidation(
pattern: r'^[A-Z0-9]+$',
minLength: 4,
maxLength: 16,
allowEmpty: false,
),
)
final String? coupon;
Numeric min/max, string or collection minLength/maxLength, regular
expression pattern, and allowEmpty run both while parsing and building.
Named constructors #
Set constructor when generated parsing should invoke a named constructor:
@DeepLink('/profiles/:name', constructor: 'fromUri')
final class ProfileLink {
const ProfileLink.fromUri(this.name);
final String name;
}
Errors and non-throwing parsing #
parse distinguishes no match, bad values, rejected constructors, and build
failures:
DeepLinkNotFoundExceptionDeepLinkFormatExceptionDeepLinkConstructionExceptionDeepLinkBuildException
Errors expose route, parameter, value, expectedType, and cause where
relevant. Their toString() intentionally omits the full URI to avoid leaking
sensitive query values into logs.
switch (AppLinks.parseResult(uri)) {
case DeepLinkSuccess(:final value):
handle(value);
case DeepLinkFailure(:final error):
report(error);
}
tryParse returns null only when no route matches; malformed matching routes
still throw, preventing bad links from being mistaken for unrelated routes.
Manifest and router adapters #
Every registry implements DeepLinkRouter<T> and exposes immutable metadata:
final router = AppLinks.router;
final routes = AppLinks.routes;
This is enough for custom routers, analytics allowlists, debug screens, and documentation tooling.
For Flutter Router:
import 'package:typed_deep_links_flutter/typed_deep_links_flutter.dart';
const parser = TypedDeepLinkRouteInformationParser<AppLink>(
router: AppLinks.router,
);
For GoRouter, parse in a redirect or top-level builder and build locations from typed values:
AppLink? parseGoRouterState(GoRouterState state) =>
AppLinks.tryParse(state.uri);
String locationFor(AppLink link) => AppLinks.toUri(link).toString();
For AutoRoute or Navigator, use the same AppLinks.parse result at the app
boundary and switch on its typed value. No router package is imported by the
runtime or generator.
Cross-library registries #
An explicit registry can import and aggregate annotated route types from many
libraries. Import route types without prefixes in the registry library, list
them in routes, and choose a shared sealed/interface baseType. This keeps
feature code separate while preserving one exhaustive app-level parser.
Complete Flutter example #
The example demonstrates canonical and legacy paths, constraints, custom codecs, optional and catch-all segments, repeated queries, enum wire names, a typed registry, route manifest, Flutter adapter, and Navigator:
cd example
flutter pub get
dart run build_runner build
flutter run
Packages #
typed_deep_links: annotations and dependency-light runtime.typed_deep_links_generator: build-time generator.typed_deep_links_flutter: optional Flutter Router API adapter.
Repository uses a Dart pub workspace. Contributors run flutter pub get once
at repository root; all packages share one lockfile and package configuration.
License #
MIT.