typed_deep_links 0.3.1 copy "typed_deep_links: ^0.3.1" to clipboard
typed_deep_links: ^0.3.1 copied to clipboard

Type-safe, router-agnostic deep links with generated URI parsing and building.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:typed_deep_links/typed_deep_links.dart';
import 'package:typed_deep_links_flutter/typed_deep_links_flutter.dart';

import 'deep_links.dart';

void main() => runApp(const TypedDeepLinksExampleApp());

const appRouteInformationParser = TypedDeepLinkRouteInformationParser<AppLink>(
  router: AppLinks.router,
);

class TypedDeepLinksExampleApp extends StatelessWidget {
  const TypedDeepLinksExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'typed_deep_links',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff5b5bd6)),
        inputDecorationTheme: const InputDecorationTheme(
          border: OutlineInputBorder(),
        ),
        useMaterial3: true,
      ),
      home: const DeepLinkPlaygroundPage(),
    );
  }
}

class DeepLinkPlaygroundPage extends StatefulWidget {
  const DeepLinkPlaygroundPage({super.key});

  @override
  State<DeepLinkPlaygroundPage> createState() => _DeepLinkPlaygroundPageState();
}

class _DeepLinkPlaygroundPageState extends State<DeepLinkPlaygroundPage> {
  final _uriController = TextEditingController(
    text: '/orders/42?source=notification',
  );
  AppLink? _parsedLink;
  DeepLinkException? _error;

  @override
  void initState() {
    super.initState();
    _parse();
  }

  @override
  void dispose() {
    _uriController.dispose();
    super.dispose();
  }

  void _parse() {
    final result = AppLinks.parseResult(Uri.parse(_uriController.text.trim()));
    setState(() {
      switch (result) {
        case DeepLinkSuccess(:final value):
          _parsedLink = value;
          _error = null;
        case DeepLinkFailure(:final error):
          _parsedLink = null;
          _error = error;
      }
    });
  }

  void _useUri(Uri uri) {
    _uriController.text = uri.toString();
    _parse();
  }

  void _openParsedLink() {
    final link = _parsedLink;
    if (link == null) return;
    Navigator.of(context).push<void>(
      MaterialPageRoute(builder: (_) => LinkDestinationPage(link: link)),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(title: const Text('typed_deep_links')),
      body: SafeArea(
        child: SelectionArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.fromLTRB(20, 12, 20, 40),
            child: Center(
              child: ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 820),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Text(
                      'Router-independent, typed deep links',
                      style: theme.textTheme.headlineMedium?.copyWith(
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Enter any URI. Generated code matches its route, '
                      'validates values, and creates a typed Dart object.',
                      style: theme.textTheme.bodyLarge?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                      ),
                    ),
                    const SizedBox(height: 24),
                    _SectionCard(
                      title: '1. Parse a URI',
                      icon: Icons.link,
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          TextField(
                            key: const ValueKey('uriInput'),
                            controller: _uriController,
                            decoration: const InputDecoration(
                              labelText: 'Deep-link URI',
                              hintText: '/orders/42?source=email',
                              prefixIcon: Icon(Icons.language),
                            ),
                            keyboardType: TextInputType.url,
                            onSubmitted: (_) => _parse(),
                          ),
                          const SizedBox(height: 12),
                          Align(
                            alignment: Alignment.centerRight,
                            child: FilledButton.icon(
                              key: const ValueKey('parseButton'),
                              onPressed: _parse,
                              icon: const Icon(Icons.bolt),
                              label: const Text('Parse link'),
                            ),
                          ),
                          const SizedBox(height: 18),
                          Text(
                            'Try a sample',
                            style: theme.textTheme.labelLarge,
                          ),
                          const SizedBox(height: 8),
                          Wrap(
                            spacing: 8,
                            runSpacing: 8,
                            children: _samples
                                .map(
                                  (sample) => ActionChip(
                                    label: Text(sample.label),
                                    onPressed: () =>
                                        _useUri(Uri.parse(sample.location)),
                                  ),
                                )
                                .toList(),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(height: 16),
                    _ParseResultCard(
                      link: _parsedLink,
                      error: _error,
                      onOpen: _openParsedLink,
                    ),
                    const SizedBox(height: 16),
                    _SectionCard(
                      title: '3. Generate URIs from typed values',
                      icon: Icons.outbound,
                      child: Column(
                        children: [
                          _GeneratedLinkTile(
                            code: "OrderLink(id: 108, source: 'email').toUri()",
                            uri: const OrderLink(
                              id: 108,
                              source: 'email',
                            ).toUri(),
                            onUse: _useUri,
                          ),
                          const Divider(height: 24),
                          _GeneratedLinkTile(
                            code:
                                "ProductLink(ProductSlug('trail-shoes'), "
                                "preview: true).toUri()",
                            uri: const ProductLink(
                              ProductSlug('trail-shoes'),
                              preview: true,
                            ).toUri(),
                            onUse: _useUri,
                          ),
                          const Divider(height: 24),
                          _GeneratedLinkTile(
                            code: "ProfileLink.fromUri('ada').toUri()",
                            uri: const ProfileLink.fromUri('ada').toUri(),
                            onUse: _useUri,
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

const _samples = <({String label, String location})>[
  (label: 'Order', location: '/orders/42?source=email'),
  (label: 'Product', location: '/products/red-shoes?preview=true'),
  (label: 'Search', location: '/search?q=boots#reviews'),
  (label: 'App scheme', location: 'myapp://open/profiles/ada'),
  (label: 'Catch-all', location: '/docs/guides/getting-started'),
  (label: 'Legacy alias', location: '/purchases/42?source=email'),
  (label: 'Invalid value', location: '/orders/not-a-number'),
];

class _ParseResultCard extends StatelessWidget {
  const _ParseResultCard({
    required this.link,
    required this.error,
    required this.onOpen,
  });

  final AppLink? link;
  final DeepLinkException? error;
  final VoidCallback onOpen;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    if (error case final error?) {
      return _SectionCard(
        title: '2. Parse failed',
        icon: Icons.error_outline,
        color: theme.colorScheme.errorContainer,
        child: Text(
          error.toString(),
          style: TextStyle(color: theme.colorScheme.onErrorContainer),
        ),
      );
    }

    final value = link;
    if (value == null) return const SizedBox.shrink();
    return _SectionCard(
      title: '2. Parsed ${value.runtimeType}',
      icon: Icons.check_circle_outline,
      color: theme.colorScheme.primaryContainer,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(_describeLink(value), style: theme.textTheme.titleMedium),
          const SizedBox(height: 6),
          Text('Round trip: ${AppLinks.toUri(value)}'),
          const SizedBox(height: 16),
          Align(
            alignment: Alignment.centerRight,
            child: FilledButton.tonalIcon(
              onPressed: onOpen,
              icon: const Icon(Icons.open_in_new),
              label: const Text('Open with Navigator'),
            ),
          ),
        ],
      ),
    );
  }
}

class _GeneratedLinkTile extends StatelessWidget {
  const _GeneratedLinkTile({
    required this.code,
    required this.uri,
    required this.onUse,
  });

  final String code;
  final Uri uri;
  final ValueChanged<Uri> onUse;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(code, style: theme.textTheme.bodySmall),
              const SizedBox(height: 5),
              Text(
                uri.toString(),
                style: theme.textTheme.titleSmall?.copyWith(
                  color: theme.colorScheme.primary,
                  fontWeight: FontWeight.w700,
                ),
              ),
            ],
          ),
        ),
        TextButton(onPressed: () => onUse(uri), child: const Text('Try it')),
      ],
    );
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.title,
    required this.icon,
    required this.child,
    this.color,
  });

  final String title;
  final IconData icon;
  final Widget child;
  final Color? color;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      color: color,
      margin: EdgeInsets.zero,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Icon(icon, size: 21),
                const SizedBox(width: 9),
                Expanded(
                  child: Text(
                    title,
                    style: theme.textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 18),
            child,
          ],
        ),
      ),
    );
  }
}

class LinkDestinationPage extends StatelessWidget {
  const LinkDestinationPage({required this.link, super.key});

  final AppLink link;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(link.runtimeType.toString())),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.route, size: 72),
              const SizedBox(height: 20),
              Text(
                _destinationTitle(link),
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              const SizedBox(height: 8),
              Text(
                'Navigator received a generated typed value—not raw URI maps.',
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

String _describeLink(AppLink link) => switch (link) {
  OrderLink(:final id, :final source) =>
    'Order id: $id${source == null ? '' : ' • source: $source'}',
  ProductLink(:final slug, :final preview) =>
    'Product slug: $slug • preview: $preview',
  SearchLink(:final query, :final section) =>
    'Search: ${query ?? '(empty)'} • section: ${section ?? '(none)'}',
  ProfileLink(:final username) => 'Profile username: $username',
  DocsLink(:final sections) => 'Docs path: ${sections.join(' / ')}',
};

String _destinationTitle(AppLink link) => switch (link) {
  OrderLink(:final id) => 'Showing order #$id',
  ProductLink(:final slug) => 'Showing product “$slug”',
  SearchLink(:final query) => 'Showing results for “${query ?? ''}”',
  ProfileLink(:final username) => 'Showing @$username',
  DocsLink(:final sections) => 'Showing docs: ${sections.join(' / ')}',
};
0
likes
160
points
183
downloads

Documentation

API reference

Publisher

verified publisherpinz.dev

Weekly Downloads

Type-safe, router-agnostic deep links with generated URI parsing and building.

Repository (GitHub)
View/report issues
Contributing

Topics

#deep-linking #code-generation #routing #uri #flutter

License

MIT (license)

Dependencies

meta

More

Packages that depend on typed_deep_links