copyable_widget 1.2.0
copyable_widget: ^1.2.0 copied to clipboard
Zero-boilerplate clipboard copy for any Flutter widget or text, with haptic feedback and SnackBar confirmation.
import 'package:copyable_widget/copyable_widget.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const CopyableExampleApp());
}
class CopyableExampleApp extends StatelessWidget {
const CopyableExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'copyable_widget demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF6750A4),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
home: const ExamplePage(),
);
}
}
class ExamplePage extends StatelessWidget {
const ExamplePage({super.key});
static const _cardNumber = '4111 1111 1111 1111';
static const _iban = 'GB29 NWBK 6016 1331 9268 19';
static const _accountNumber = 'DE89 3704 0044 0532 0130 00';
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
title: const Text('copyable_widget'),
centerTitle: false,
backgroundColor: cs.surfaceContainerHighest,
scrolledUnderElevation: 0,
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
children: [
// ── 1. Copyable.text with value ──────────────────────────────────
const _SectionLabel('Copyable.text — label + value'),
const SizedBox(height: 8),
_DemoCard(
description:
'The label "Copy card number" is displayed; the actual '
'card number is what lands on the clipboard.',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_cardNumber,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 16,
fontWeight: FontWeight.w600,
color: cs.onSurface,
letterSpacing: 1.5,
),
),
const SizedBox(height: 8),
Copyable.text(
'Copy card number',
value: _cardNumber,
feedback: const CopyableFeedback.snackBar(
text: 'Card number copied!',
),
style: TextStyle(
fontWeight: FontWeight.w600,
color: cs.primary,
),
),
],
),
),
const SizedBox(height: 24),
// ── 2. Whole row copyable ────────────────────────────────────────
const _SectionLabel('Copyable — whole row'),
const SizedBox(height: 8),
const _DemoCard(
description:
'Wrap the entire row — tap anywhere on it to copy.',
child: _IbanRow(iban: _iban),
),
const SizedBox(height: 24),
// ── 3. Only the copy icon is copyable ────────────────────────────
const _SectionLabel('Copyable — icon only'),
const SizedBox(height: 8),
const _DemoCard(
description:
'Only the copy icon is wrapped with Copyable — the rest '
'of the row is not interactive.',
child: _IbanRowIconOnly(iban: _accountNumber),
),
const SizedBox(height: 24),
// ── 4. clearAfter ─────────────────────────────────────────────────
const _SectionLabel('clearAfter — automatic clipboard security'),
const SizedBox(height: 8),
_DemoCard(
description:
'Copies the card number to the clipboard, then clears it '
'after 10 seconds. Ideal for FinTech and crypto apps.',
child: Copyable.text(
'Copy sensitive value',
value: _cardNumber,
clearAfter: const Duration(seconds: 10),
feedback: const CopyableFeedback.snackBar(
text: 'Copied! Clipboard clears in 10 s',
),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.error,
),
),
),
const SizedBox(height: 24),
// ── 5. onError ───────────────────────────────────────────────────
const _SectionLabel('onError — clipboard error handling'),
const SizedBox(height: 8),
_DemoCard(
description:
'The onError callback is called if Clipboard.setData throws. '
'In this demo the error is printed to the console.',
child: Copyable.text(
'TXN-9182736',
onError: (e) => debugPrint('Clipboard error: $e'),
feedback: const CopyableFeedback.snackBar(
text: 'Transaction ID copied',
),
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const SizedBox(height: 24),
// ── 6. CopyableTheme ─────────────────────────────────────────────
const _SectionLabel('CopyableTheme — app-wide defaults'),
const SizedBox(height: 8),
_DemoCard(
description:
'CopyableTheme sets snackBarText, snackBarDuration, and '
'clearAfter for all descendants. Per-widget values override '
'the theme.',
child: CopyableTheme(
data: const CopyableThemeData(
snackBarText: 'Copied via theme!',
snackBarDuration: Duration(seconds: 3),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Copyable.text(
'Uses theme snackBar text',
feedback: const CopyableFeedback.snackBar(),
),
const SizedBox(height: 8),
Copyable.text(
'Overrides theme text',
feedback: const CopyableFeedback.snackBar(
text: 'Custom override!',
),
),
],
),
),
),
const SizedBox(height: 24),
// ── 7. CopyableBuilder ───────────────────────────────────────────
const _SectionLabel('CopyableBuilder — custom isCopied UI'),
const SizedBox(height: 8),
const _DemoCard(
description:
'CopyableBuilder exposes an isCopied boolean to build '
'fully custom copy UI — GitHub-style icon toggle shown here.',
child: _CopyableBuilderDemo(value: _iban),
),
const SizedBox(height: 40),
],
),
);
}
}
// ── Supporting widgets ────────────────────────────────────────────────────────
class _IbanRow extends StatelessWidget {
const _IbanRow({required this.iban});
final String iban;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Copyable(
value: iban,
feedback: const CopyableFeedback.snackBar(text: 'IBAN copied!'),
child: Row(
children: [
Icon(Icons.account_balance_rounded, color: cs.primary, size: 20),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'IBAN',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: cs.outline),
),
Text(
iban,
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
color: cs.onSurface,
),
),
],
),
const Spacer(),
Icon(Icons.copy_rounded, color: cs.outline, size: 18),
],
),
);
}
}
class _IbanRowIconOnly extends StatelessWidget {
const _IbanRowIconOnly({required this.iban});
final String iban;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Row(
children: [
Icon(Icons.account_balance_rounded, color: cs.primary, size: 20),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'IBAN',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: cs.outline),
),
Text(
iban,
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
color: cs.onSurface,
),
),
],
),
const Spacer(),
Copyable(
value: iban,
feedback: const CopyableFeedback.snackBar(text: 'IBAN copied!'),
child: Icon(Icons.copy_rounded, color: cs.outline, size: 18),
),
],
);
}
}
class _CopyableBuilderDemo extends StatelessWidget {
const _CopyableBuilderDemo({required this.value});
final String value;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Row(
children: [
Expanded(
child: Text(
value,
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.w600,
color: cs.onSurface,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
CopyableBuilder(
value: value,
resetAfter: const Duration(seconds: 2),
builder: (context, isCopied) => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: isCopied
? Icon(
Icons.check_rounded,
key: const ValueKey('check'),
color: cs.primary,
size: 20,
)
: Icon(
Icons.copy_rounded,
key: const ValueKey('copy'),
color: cs.outline,
size: 20,
),
),
),
],
);
}
}
class _SectionLabel extends StatelessWidget {
const _SectionLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
);
}
}
class _DemoCard extends StatelessWidget {
const _DemoCard({required this.description, required this.child});
final String description;
final Widget child;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outlineVariant),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
child,
const SizedBox(height: 12),
Text(
description,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: cs.outline),
),
],
),
),
);
}
}