flutter_redirectly 2.1.2
flutter_redirectly: ^2.1.2 copied to clipboard
A Flutter plugin for Redirectly dynamic links - similar to Firebase Dynamic Links but using your own backend.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_redirectly/flutter_redirectly.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Redirectly Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const RedirectlyExample(),
);
}
}
class RedirectlyExample extends StatefulWidget {
const RedirectlyExample({super.key});
@override
State<RedirectlyExample> createState() => _RedirectlyExampleState();
}
class _RedirectlyExampleState extends State<RedirectlyExample> {
final FlutterRedirectly _redirectly = FlutterRedirectly();
final TextEditingController _slugController = TextEditingController();
final TextEditingController _targetController = TextEditingController();
final TextEditingController _apiKeyController =
TextEditingController(text: "20c9cbb4-7931-43f9-bce3-f7cf0f1f5911");
bool _isInitialized = false;
bool _isLoading = false;
String _status = 'Not initialized';
final List<String> _linkHistory = [];
RedirectlyLinkClick? _lastLinkClick;
RedirectlyLink? _lastCreatedLink;
RedirectlyTempLink? _lastCreatedTempLink;
RedirectlyAppInstallResponse? _lastAppInstall;
@override
void initState() {
super.initState();
_targetController.text = 'https://example.com';
}
@override
void dispose() {
_slugController.dispose();
_targetController.dispose();
_apiKeyController.dispose();
super.dispose();
}
Future<void> _initializeRedirectly() async {
if (_apiKeyController.text.isEmpty) {
_showSnackBar('Please enter your API key', isError: true);
return;
}
setState(() {
_isLoading = true;
_status = 'Initializing...';
});
try {
await _redirectly.initialize(RedirectlyConfig(
apiKey: _apiKeyController.text,
enableDebugLogging: true,
));
// Listen for incoming links when app is running
_redirectly.onLinkClick.listen((linkClick) {
setState(() {
_lastLinkClick = linkClick;
_linkHistory.insert(0, 'App Running: ${linkClick.originalUrl}');
});
_showSnackBar('Link received: ${linkClick.slug}');
});
// Listen for app install events
_redirectly.onAppInstalled.listen((appInstall) {
setState(() {
_lastAppInstall = appInstall;
});
if (appInstall.matched) {
_showSnackBar(
'App install matched to referral: ${appInstall.username}/${appInstall.slug}');
} else {
_showSnackBar('App install tracked as organic');
}
});
// Check for initial link (when app was opened via link)
final initialLink = await _redirectly.getInitialLink();
if (initialLink != null) {
setState(() {
_lastLinkClick = initialLink;
_linkHistory.insert(0, 'App Launch: ${initialLink.originalUrl}');
});
_showSnackBar('Initial link detected: ${initialLink.slug}');
}
setState(() {
_isInitialized = true;
_status = 'Ready! Listening for links...';
});
_showSnackBar('Redirectly initialized successfully!');
} on RedirectlyError catch (e) {
setState(() {
_status = 'Error: ${e.message}';
});
_showSnackBar('Initialization failed: ${e.message}', isError: true);
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _createPermanentLink() async {
if (_slugController.text.isEmpty || _targetController.text.isEmpty) {
_showSnackBar('Please enter both slug and target URL', isError: true);
return;
}
setState(() => _isLoading = true);
try {
final link = await _redirectly.createLink(
slug: _slugController.text,
target: _targetController.text,
metadata: {'created_from': 'flutter_example'},
);
setState(() {
_lastCreatedLink = link;
});
await Clipboard.setData(ClipboardData(text: link.url));
_showSnackBar('Permanent link created and copied to clipboard!');
_slugController.clear();
} on RedirectlyError catch (e) {
_showSnackBar('Failed to create link: ${e.message}', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _createTempLink() async {
if (_targetController.text.isEmpty) {
_showSnackBar('Please enter target URL', isError: true);
return;
}
setState(() => _isLoading = true);
try {
final tempLink = await _redirectly.createTempLink(
target: _targetController.text,
slug: _slugController.text.isNotEmpty ? _slugController.text : null,
ttlSeconds: 3600, // 1 hour
);
setState(() {
_lastCreatedTempLink = tempLink;
});
await Clipboard.setData(ClipboardData(text: tempLink.url));
_showSnackBar('Temporary link created and copied to clipboard!');
_slugController.clear();
} on RedirectlyError catch (e) {
_showSnackBar('Failed to create temp link: ${e.message}', isError: true);
} finally {
setState(() => _isLoading = false);
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
duration: const Duration(seconds: 3),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Redirectly Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Card
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(_status),
],
),
),
),
const SizedBox(height: 16),
// Initialization Section
if (!_isInitialized) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Initialize Redirectly',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
TextField(
controller: _apiKeyController,
decoration: const InputDecoration(
labelText: 'API Key',
hintText: 'Enter your Redirectly API key',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _initializeRedirectly,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Initialize'),
),
),
],
),
),
),
],
// Link Creation Section
if (_isInitialized) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create Links',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
TextField(
controller: _slugController,
decoration: const InputDecoration(
labelText: 'Slug (optional for temp links)',
hintText: 'my-awesome-link',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _targetController,
decoration: const InputDecoration(
labelText: 'Target URL',
hintText: 'https://example.com',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed:
_isLoading ? null : _createPermanentLink,
child: const Text('Create Permanent Link'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _createTempLink,
child: const Text('Create Temp Link (1h)'),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Last Created Links
if (_lastCreatedLink != null || _lastCreatedTempLink != null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Created Links',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (_lastCreatedLink != null) ...[
Text('Permanent Link:',
style: Theme.of(context).textTheme.bodySmall),
SelectableText(_lastCreatedLink!.url),
Text('Clicks: ${_lastCreatedLink!.clickCount}'),
const SizedBox(height: 8),
],
if (_lastCreatedTempLink != null) ...[
Text('Temporary Link:',
style: Theme.of(context).textTheme.bodySmall),
SelectableText(_lastCreatedTempLink!.url),
Text('Expires: ${_lastCreatedTempLink!.expiresAt}'),
],
],
),
),
),
const SizedBox(height: 16),
],
// App Install Information
if (_lastAppInstall != null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'App Install Information',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text('Type: ${_lastAppInstall!.type}'),
Text('Matched: ${_lastAppInstall!.matched}'),
if (_lastAppInstall!.username != null)
Text('Username: ${_lastAppInstall!.username}'),
if (_lastAppInstall!.slug != null)
Text('Slug: ${_lastAppInstall!.slug}'),
if (_lastAppInstall!.link != null) ...[
const SizedBox(height: 8),
Text('Referral Link:',
style: Theme.of(context).textTheme.bodySmall),
SelectableText(_lastAppInstall!.link!.url),
Text('Target: ${_lastAppInstall!.link!.target}'),
Text('Created: ${_lastAppInstall!.link!.createdAt}'),
],
],
),
),
),
const SizedBox(height: 16),
],
// Last Received Link
if (_lastLinkClick != null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Last Received Link',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text('URL: ${_lastLinkClick!.originalUrl}'),
Text('Username: ${_lastLinkClick!.username}'),
Text('Slug: ${_lastLinkClick!.slug}'),
Text('Received: ${_lastLinkClick!.receivedAt}'),
if (_lastLinkClick!.error != null)
Text(
'Error: ${_lastLinkClick!.error!.message}',
style: const TextStyle(color: Colors.red),
),
],
),
),
),
const SizedBox(height: 16),
],
// Link History
if (_linkHistory.isNotEmpty) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Link History',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
..._linkHistory.take(5).map((link) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
link,
style: Theme.of(context).textTheme.bodySmall,
),
)),
],
),
),
),
],
],
],
),
),
);
}
}