dynalinks 2.0.0
dynalinks: ^2.0.0 copied to clipboard
Dynalinks SDK for Flutter - Deferred deep linking and attribution for iOS and Android apps.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dynalinks/dynalinks.dart';
/// Skips awaiting `configure` before `runApp`, so `getInitialLink()` can race the SDK becoming
/// ready. That race is the one a cold-start link has to survive: the platform hands the link over
/// before Dart has run. Set `--dart-define=DYNALINKS_DEFER_CONFIGURE=true` to exercise it.
const _deferConfigure = bool.fromEnvironment('DYNALINKS_DEFER_CONFIGURE');
Future<void> _configure() async {
try {
const baseURL = String.fromEnvironment('DYNALINKS_BASE_URL');
await Dynalinks.configure(
clientAPIKey: const String.fromEnvironment(
'DYNALINKS_API_KEY',
defaultValue: 'your-api-key-here',
),
// Optional: point at a staging or local API. Defaults to production.
baseURL: baseURL.isEmpty ? null : baseURL,
logLevel: DynalinksLogLevel.debug,
allowSimulatorOrEmulator: true, // Allow testing on simulator
);
} on DynalinksException catch (e) {
debugPrint('Failed to configure Dynalinks: $e');
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (_deferConfigure) {
// Deliberately not awaited: the app starts, and configure lands mid-flight.
unawaited(_configure());
} else {
await _configure();
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dynalinks Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const DeepLinkPage(),
);
}
}
class DeepLinkPage extends StatefulWidget {
const DeepLinkPage({super.key});
@override
State<DeepLinkPage> createState() => _DeepLinkPageState();
}
class _DeepLinkPageState extends State<DeepLinkPage> {
DeepLinkResult? _result;
String? _error;
bool _isLoading = false;
StreamSubscription<DeepLinkResult>? _linkSubscription;
/// Which channel delivered [_result]. A launch link only ever arrives via
/// `getInitialLink()`; a link tapped while the app runs only ever arrives on
/// the stream.
String? _source;
@override
void initState() {
super.initState();
_checkInitialLink();
_listenForDeepLinks();
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
Future<void> _checkInitialLink() async {
// Check for cold start link
try {
final initialLink = await Dynalinks.getInitialLink();
if (!mounted) return;
if (initialLink != null) {
debugPrint('[example] link delivered via getInitialLink (cold start)');
_source = 'Cold start - getInitialLink()';
_handleDeepLinkResult(initialLink);
return;
}
debugPrint('[example] getInitialLink returned null');
} on DynalinksException catch (e) {
debugPrint('Error getting initial link: $e');
}
// Check for deferred deep link
_checkForDeferredDeepLink();
}
Future<void> _readInitialLinkAgain() async {
try {
final link = await Dynalinks.getInitialLink();
if (!mounted) return;
debugPrint('[example] second getInitialLink returned ${link ?? 'null'}');
setState(() {
_error = null;
_source = link == null
? 'Second read - empty, as expected'
: 'Second read - REPLAYED, this is a bug';
_result = link ?? _result;
});
} on DynalinksException catch (e) {
if (!mounted) return;
setState(() => _error = e.message);
}
}
void _listenForDeepLinks() {
_linkSubscription = Dynalinks.onDeepLinkReceived.listen(
(result) {
debugPrint('[example] link delivered via stream (app running)');
_source = 'While running - onDeepLinkReceived';
_handleDeepLinkResult(result);
},
onError: (error) {
debugPrint('Deep link stream error: $error');
},
);
}
Future<void> _checkForDeferredDeepLink() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await Dynalinks.checkForDeferredDeepLink();
if (!mounted) return;
debugPrint('[example] deferred deep link check returned');
_source = 'Deferred - checkForDeferredDeepLink()';
_handleDeepLinkResult(result);
} on SimulatorException {
if (!mounted) return;
setState(() {
_error = 'Deferred deep linking not available on simulator/emulator';
_isLoading = false;
});
} on DynalinksException catch (e) {
if (!mounted) return;
setState(() {
_error = e.message;
_isLoading = false;
});
}
}
void _handleDeepLinkResult(DeepLinkResult result) {
// Every path here crosses an await or a stream event, and the native side can hold a reply
// for as long as its watchdog allows. The widget may be gone by the time one lands.
if (!mounted) return;
setState(() {
_result = result;
_isLoading = false;
_error = null;
});
if (result.matched && result.link?.deepLinkValue != null) {
_showDeepLinkDialog(result);
}
}
void _showDeepLinkDialog(DeepLinkResult result) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Deep Link Received'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Navigate to: ${result.link?.deepLinkValue}'),
if (result.confidence != null)
Text('Confidence: ${result.confidence!.name}'),
if (result.matchScore != null) Text('Score: ${result.matchScore}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Dynalinks Example'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SDK Status',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text('Version: ${Dynalinks.version}'),
],
),
),
),
const SizedBox(height: 16),
// Result/Error Card
if (_isLoading)
const Card(
child: Padding(
padding: EdgeInsets.all(32),
child: Center(child: CircularProgressIndicator()),
),
)
else if (_error != null)
Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Error',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onErrorContainer,
),
),
const SizedBox(height: 8),
Text(
_error!,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
],
),
),
)
else if (_result != null)
_buildResultCard()
else
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('No deep link result yet'),
),
),
const SizedBox(height: 24),
// Actions
FilledButton.icon(
onPressed: _checkForDeferredDeepLink,
icon: const Icon(Icons.refresh),
label: const Text('Check for Deferred Deep Link'),
),
const SizedBox(height: 8),
// Reads the initial link a second time. It must come back empty: a launch link is
// handed over once.
OutlinedButton.icon(
onPressed: _readInitialLinkAgain,
icon: const Icon(Icons.replay),
label: const Text('Read Initial Link Again'),
),
],
),
),
);
}
Widget _buildResultCard() {
final result = _result!;
final link = result.link;
return Card(
color: result.matched
? Theme.of(context).colorScheme.primaryContainer
: null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
result.matched ? 'Match Found!' : 'No Match',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: result.matched
? Theme.of(context).colorScheme.onPrimaryContainer
: null,
),
),
const Divider(),
if (_source != null) _buildInfoRow('Delivered via', _source!),
_buildInfoRow('Matched', result.matched.toString()),
if (result.confidence != null)
_buildInfoRow('Confidence', result.confidence!.name),
if (result.matchScore != null)
_buildInfoRow('Score', result.matchScore.toString()),
_buildInfoRow('Deferred', result.isDeferred.toString()),
if (link != null) ...[
const Divider(),
Text('Link Data', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
_buildInfoRow('ID', link.id),
if (link.name != null) _buildInfoRow('Name', link.name!),
if (link.path != null) _buildInfoRow('Path', link.path!),
if (link.deepLinkValue != null)
_buildInfoRow('Deep Link Value', link.deepLinkValue!),
if (link.fullUrl != null)
_buildInfoRow('Full URL', link.fullUrl.toString()),
],
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(child: Text(value)),
],
),
);
}
}