copyable_widget 1.2.0 copy "copyable_widget: ^1.2.0" to clipboard
copyable_widget: ^1.2.0 copied to clipboard

Zero-boilerplate clipboard copy for any Flutter widget or text, with haptic feedback and SnackBar confirmation.

example/lib/main.dart

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),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
241
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Zero-boilerplate clipboard copy for any Flutter widget or text, with haptic feedback and SnackBar confirmation.

Repository (GitHub)
View/report issues

Topics

#clipboard #clipboard-widget #copy #copyable #copyable-widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on copyable_widget