smart_links 1.0.0
smart_links: ^1.0.0 copied to clipboard
Production-grade deep linking for Android and Flutter Web with deferred install referrer support.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:smart_links/smart_links.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const SmartLinksExampleApp());
}
class SmartLinksExampleApp extends StatelessWidget {
const SmartLinksExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Smart Links Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1565C0)),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _log = <String>[];
SmartLinkData? _initial;
SmartLinkData? _deferred;
bool _ready = false;
StreamSubscription<SmartLinkData>? _sub;
@override
void initState() {
super.initState();
_bootstrap();
}
Future<void> _bootstrap() async {
await SmartLink.initialize(enableLogs: kDebugMode);
_initial = await SmartLink.initialLink;
_deferred = await SmartLink.deferredLink;
_sub = SmartLink.stream.listen((link) {
_add('Stream: ${link.path} (${link.source.name})');
setState(() {});
});
if (_initial != null) {
_add('Initial: ${_initial!.path}');
}
if (_deferred != null) {
_add('Deferred: ${_deferred!.path}');
}
setState(() {});
}
void _add(String line) {
setState(() {
_log.insert(0, '${DateTime.now().toIso8601String().substring(11, 19)} $line');
});
}
void _markReady() {
if (_ready) return;
SmartLink.markReady();
_ready = true;
_add('markReady() — queued links replayed');
setState(() {});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final sampleReferrer = SmartLinkParser.encodeReferrerPayload(
path: '/product/42',
params: {'campaign': 'summer'},
);
return Scaffold(
appBar: AppBar(
title: const Text('Smart Links'),
actions: [
IconButton(
tooltip: 'Mark app ready',
onPressed: _ready ? null : _markReady,
icon: Icon(_ready ? Icons.check_circle : Icons.play_circle_outline),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_StatusCard(
ready: _ready,
initial: _initial,
deferred: _deferred,
),
const SizedBox(height: 12),
_InfoCard(
title: 'Try these links',
children: [
if (kIsWeb) ...[
const Text('Change the browser URL path or hash, e.g.'),
SelectableText(Uri.base.replace(path: '/product/99').toString()),
] else ...[
const Text('Custom scheme (adb):'),
SelectableText(
'adb shell am start -a android.intent.action.VIEW '
'-d "smartlinkdemo://product/42?ref=demo"',
),
const SizedBox(height: 8),
const Text('HTTPS App Link (after domain verification):'),
const SelectableText(
'https://smartlinkdemo.example.com/product/42?campaign=summer',
),
],
],
),
const SizedBox(height: 12),
_InfoCard(
title: 'Deferred referrer payload (Play Store)',
children: [
const Text(
'Pass as Play Store referrer (utm_content or payload param):',
),
const SizedBox(height: 8),
SelectableText(sampleReferrer),
const SizedBox(height: 8),
SelectableText(
'https://play.google.com/store/apps/details?id=com.example.example'
'&referrer=${Uri.encodeComponent('utm_content=$sampleReferrer')}',
),
],
),
const SizedBox(height: 12),
Text('Event log', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (_log.isEmpty)
const Text('No events yet. Open a deep link or tap Mark Ready.')
else
..._log.map((e) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(e, style: const TextStyle(fontFamily: 'monospace')),
)),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _ready ? null : _markReady,
icon: const Icon(Icons.rocket_launch),
label: Text(_ready ? 'App ready' : 'Mark ready'),
),
);
}
}
class _StatusCard extends StatelessWidget {
const _StatusCard({
required this.ready,
required this.initial,
required this.deferred,
});
final bool ready;
final SmartLinkData? initial;
final SmartLinkData? deferred;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
ready ? Icons.check_circle : Icons.hourglass_top,
color: ready ? Colors.green : Colors.orange,
),
const SizedBox(width: 8),
Text(
ready ? 'Router ready' : 'Queueing links until markReady()',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const Divider(),
_row('Initial link', initial?.path ?? '—'),
_row('Deferred link', deferred?.path ?? '—'),
if (initial != null)
_row('Initial params', initial!.queryParams.toString()),
],
),
),
);
}
Widget _row(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 110, child: Text(label, style: const TextStyle(fontWeight: FontWeight.w600))),
Expanded(child: Text(value)),
],
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({required this.title, required this.children});
final String title;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
...children,
],
),
),
);
}
}