You're Using Flutter's Clipboard Wrong (And Missing These 10 Powerful Features) (Part 2)
FlutterPulseSnackBar(content: Text('Copied!')),
);
}
5. Use Appropriate Icons
// Different content types should have different icons
Icons.copy // Generic text
Icons.link // URLs
Icons.email // Email addresses
Icons.phone // Phone numbers
Icons.lock // Sensitive data
Icons.palette // Colors
Performance Tips
Tip 1: Debounce Clipboard Checks
// Don't check clipboard on every frame!
Timer? _debounceTimer;
void _checkClipboard() {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 300), () {
// Check clipboard here
});
}
Tip 2: Use autoDispose for Providers
// ✅ Always use autoDispose for clipboard providers
final clipboardProvider = StateProvider.autoDispose<String?>((ref) => null);
Tip 3: Limit History Size
// Don't store unlimited clipboard history
static const _maxHistorySize = 20;
if (history.length > _maxHistorySize) {
history.removeRange(_maxHistorySize, history.length);
}
Tip 4: Compress Large Text
// For very large text, consider compression
import 'dart:convert';
import 'dart:io';
Future<String> compressText(String text) async {
if (text.length < 1000) return text;
final bytes = utf8.encode(text);
final compressed = gzip.encode(bytes);
return base64Encode(compressed);
}
Security Considerations
1. Never Log Clipboard Content
// ❌ NEVER DO THIS
print('Clipboard content: ${clipboardText}');
// ✅ DO THIS
print('Clipboard operation completed');
2. Clear Sensitive Data
// Always clear passwords, tokens, etc.
await Clipboard.setData(ClipboardData(text: password));
Timer(Duration(seconds: 30), () {
Clipboard.setData(ClipboardData(text: ''));
});
3. Warn Users About Auto-Clear
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Copied! Will auto-clear in 30s'),
backgroundColor: Colors.orange,
),
);
4. Be Careful with Clipboard Monitoring
// Only monitor if user explicitly enables it
final monitoringEnabled = ref.watch(clipboardMonitoringEnabledProvider);
if (!monitoringEnabled) {
return; // Don't monitor
}
UX Tips That Make a Difference
1. Haptic Feedback
import 'package:flutter/services.dart';
await Clipboard.setData(ClipboardData(text: text));
HapticFeedback.mediumImpact(); // Feels nice!
2. Toast Instead of Snackbar (Sometimes)
// For subtle feedback
// Use fluttertoast package
Fluttertoast.showToast(
msg: "Copied!",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
3. Preview Before Copy
// Show dialog with preview
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Copy this?'),
content: SelectableText(textToPreview),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: textToPreview));
Navigator.pop(context);
},
child: Text('Copy'),
),
],
),
);
4. Animate Success State
class AnimatedCopyButton extends ConsumerStatefulWidget {
final String text;
@override
ConsumerState<AnimatedCopyButton> createState() => _AnimatedCopyButtonState();
}
class _AnimatedCopyButtonState extends ConsumerState<AnimatedCopyButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
bool _copied = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _copy() async {
await Clipboard.setData(ClipboardData(text: widget.text));
setState(() => _copied = true);
_controller.forward().then((_) => _controller.reverse());
Future.delayed(Duration(seconds: 2), () {
if (mounted) setState(() => _copied = false);
});
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: ElevatedButton.icon(
onPressed: _copy,
icon: Icon(_copied ? Icons.check : Icons.copy),
label: Text(_copied ? 'Copied!' : 'Copy'),
style: ElevatedButton.styleFrom(
backgroundColor: _copied ? Colors.green : null,
),
),
);
}
}Testing Your Clipboard Code
// test/clipboard_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
group('Clipboard Service Tests', () {
test('Copy text saves to state', () async {
final container = ProviderContainer();
await container.read(clipboardServiceProvider).copyText('Hello');
final lastCopied = container.read(lastCopiedTextProvider);
expect(lastCopied, 'Hello');
container.dispose();
});
test('Feedback shows and hides', () async {
final container = ProviderContainer();
// Initially false
expect(container.read(showCopiedFeedbackProvider), false);
// Copy triggers feedback
await container.read(clipboardServiceProvider).copyText('Test');
expect(container.read(showCopiedFeedbackProvider), true);
// Wait for auto-hide
await Future.delayed(Duration(seconds: 3));
expect(container.read(showCopiedFeedbackProvider), false);
container.dispose();
});
test('Clipboard history adds items correctly', () {
final container = ProviderContainer();
// Add items
container.read(clipboardHistoryProvider.notifier).addItem(
'Item 1',
ClipboardContentType.text,
);
container.read(clipboardHistoryProvider.notifier).addItem(
'Item 2',
ClipboardContentType.url,
);
final history = container.read(clipboardHistoryProvider);
expect(history.length, 2);
expect(history.first.text, 'Item 2'); // Most recent first
expect(history.last.text, 'Item 1');
container.dispose();
});
test('History removes duplicates', () {
final container = ProviderContainer();
// Add same item twice
container.read(clipboardHistoryProvider.notifier).addItem(
'Duplicate',
ClipboardContentType.text,
);
container.read(clipboardHistoryProvider.notifier).addItem(
'Duplicate',
ClipboardContentType.text,
);
final history = container.read(clipboardHistoryProvider);
expect(history.length, 1); // Only one instance
container.dispose();
});
});
}
Common Issues & Solutions
Issue 1: Clipboard Not Working on Web
Problem: Clipboard API might need permissions on web.
Solution:
// Check if clipboard is available
Future<bool> isClipboardAvailable() async {
try {
await Clipboard.getData(Clipboard.kTextPlain);
return true;
} catch (e) {
return false;
}
}
// Show message if not available
if (!await isClipboardAvailable()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Clipboard not available')),
);
return;
}
Issue 2: Paste Not Working
Problem: No data in clipboard or wrong format.
Solution:
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text == null || data!.text!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Nothing to paste')),
);
return;
}
// Use the data
final text = data.text!;
Issue 3: Memory Leak with Clipboard History
Problem: History grows too large.
Solution:
// Limit history size
static const _maxHistorySize = 20;
void addItem(String text, ClipboardContentType type) {
state = [
newItem,
...state.take(_maxHistorySize - 1), // Only keep last N items
];
}
Issue 4: Sensitive Data Not Cleared
Problem: Timer doesn't fire.
Solution:
// Keep reference to timer
Timer? _clearTimer;
@override
void dispose() {
_clearTimer?.cancel(); // Cancel on dispose
super.dispose();
}
Future<void> copySecure(String text) async {
_clearTimer?.cancel(); // Cancel existing timer
await Clipboard.setData(ClipboardData(text: text));
_clearTimer = Timer(Duration(seconds: 30), () {
Clipboard.setData(ClipboardData(text: ''));
});
}
Quick Reference Cheat Sheet
Basic Operations
// Copy
await Clipboard.setData(ClipboardData(text: 'Hello'));
// Paste
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
// Check if has data
final hasData = (await Clipboard.getData(Clipboard.kTextPlain))?.text?.isNotEmpty ?? false;
// Clear
await Clipboard.setData(ClipboardData(text: ''));
With Riverpod
// Copy with state management
await ref.read(clipboardServiceProvider).copyText('Hello');
// Paste
final text = await ref.read(clipboardServiceProvider).pasteText();
// Check last copied
final lastCopied = ref.watch(lastCopiedTextProvider);
// Check if showing feedback
final showFeedback = ref.watch(showCopiedFeedbackProvider);
Content Type Detection
// URL
bool isUrl = Uri.tryParse(text)?.hasAbsolutePath ?? false;
bool isEmail = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}
return SelectableText(
text,
contextMenuBuilder: (context, editableTextState) {
return AdaptiveTextSelectionToolbar(
anchors: editableTextState.contextMenuAnchors,
children: [
// Copy button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
final selection = editableTextState.textEditingValue.selection;
final selectedText = text.substring(
selection.start,
selection.end,
);
Clipboard.setData(ClipboardData(text: selectedText));
editableTextState.hideToolbar();
},
child: Text('Copy'),
),
// Select All button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
editableTextState.selectAll(SelectionChangedCause.toolbar);
},
child: Text('Select All'),
),
],
);
},
);
}
}
Pattern 10: Share Extension (Copy + Share)
Combine clipboard with sharing:
// Need share_plus package
// pubspec.yaml:
// share_plus: ^7.2.1
import 'package:share_plus/share_plus.dart';
class ShareClipboardButton extends ConsumerWidget {
final String text;
final String? subject;
const ShareClipboardButton({
super.key,
required this.text,
this.subject,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
onSelected: (value) async {
switch (value) {
case 'copy':
await ref.read(clipboardServiceProvider).copyText(text);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Copied!')),
);
}
break;
case 'share':
await Share.share(
text,
subject: subject,
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'copy',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 12),
Text('Copy'),
],
),
),
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share),
SizedBox(width: 12),
Text('Share'),
],
),
),
],
);
}
}
Complete Example: Putting It All Together
Here's a full-featured clipboard demo app:
// screens/clipboard_demo_screen.dart
class ClipboardDemoScreen extends ConsumerStatefulWidget {
@override
ConsumerState<ClipboardDemoScreen> createState() => _ClipboardDemoScreenState();
}
class _ClipboardDemoScreenState extends ConsumerState<ClipboardDemoScreen> {
final _textController = TextEditingController();
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget buil).hasMatch(text);
// Phone
bool isPhone = RegExp(r'^\+?[\d\s\-\(\)]+
return SelectableText(
text,
contextMenuBuilder: (context, editableTextState) {
return AdaptiveTextSelectionToolbar(
anchors: editableTextState.contextMenuAnchors,
children: [
// Copy button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
final selection = editableTextState.textEditingValue.selection;
final selectedText = text.substring(
selection.start,
selection.end,
);
Clipboard.setData(ClipboardData(text: selectedText));
editableTextState.hideToolbar();
},
child: Text('Copy'),
),
// Select All button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
editableTextState.selectAll(SelectionChangedCause.toolbar);
},
child: Text('Select All'),
),
],
);
},
);
}
}
Pattern 10: Share Extension (Copy + Share)
Combine clipboard with sharing:
// Need share_plus package
// pubspec.yaml:
// share_plus: ^7.2.1
import 'package:share_plus/share_plus.dart';
class ShareClipboardButton extends ConsumerWidget {
final String text;
final String? subject;
const ShareClipboardButton({
super.key,
required this.text,
this.subject,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
onSelected: (value) async {
switch (value) {
case 'copy':
await ref.read(clipboardServiceProvider).copyText(text);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Copied!')),
);
}
break;
case 'share':
await Share.share(
text,
subject: subject,
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'copy',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 12),
Text('Copy'),
],
),
),
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share),
SizedBox(width: 12),
Text('Share'),
],
),
),
],
);
}
}
Complete Example: Putting It All Together
Here's a full-featured clipboard demo app:
// screens/clipboard_demo_screen.dart
class ClipboardDemoScreen extends ConsumerStatefulWidget {
@override
ConsumerState<ClipboardDemoScreen> createState() => _ClipboardDemoScreenState();
}
class _ClipboardDemoScreenState extends ConsumerState<ClipboardDemoScreen> {
final _textController = TextEditingController();
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget buil).hasMatch(text.trim());
// Color
bool isColor = RegExp(r'^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})
return SelectableText(
text,
contextMenuBuilder: (context, editableTextState) {
return AdaptiveTextSelectionToolbar(
anchors: editableTextState.contextMenuAnchors,
children: [
// Copy button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
final selection = editableTextState.textEditingValue.selection;
final selectedText = text.substring(
selection.start,
selection.end,
);
Clipboard.setData(ClipboardData(text: selectedText));
editableTextState.hideToolbar();
},
child: Text('Copy'),
),
// Select All button
TextSelectionToolbarTextButton(
padding: EdgeInsets.all(8),
onPressed: () {
editableTextState.selectAll(SelectionChangedCause.toolbar);
},
child: Text('Select All'),
),
],
);
},
);
}
}
Pattern 10: Share Extension (Copy + Share)
Combine clipboard with sharing:
// Need share_plus package
// pubspec.yaml:
// share_plus: ^7.2.1
import 'package:share_plus/share_plus.dart';
class ShareClipboardButton extends ConsumerWidget {
final String text;
final String? subject;
const ShareClipboardButton({
super.key,
required this.text,
this.subject,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
onSelected: (value) async {
switch (value) {
case 'copy':
await ref.read(clipboardServiceProvider).copyText(text);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Copied!')),
);
}
break;
case 'share':
await Share.share(
text,
subject: subject,
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'copy',
child: Row(
children: [
Icon(Icons.copy),
SizedBox(width: 12),
Text('Copy'),
],
),
),
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share),
SizedBox(width: 12),
Text('Share'),
],
),
),
],
);
}
}
Complete Example: Putting It All Together
Here's a full-featured clipboard demo app:
// screens/clipboard_demo_screen.dart
class ClipboardDemoScreen extends ConsumerStatefulWidget {
@override
ConsumerState<ClipboardDemoScreen> createState() => _ClipboardDemoScreenState();
}
class _ClipboardDemoScreenState extends ConsumerState<ClipboardDemoScreen> {
final _textController = TextEditingController();
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget buil).hasMatch(text);
Wrapping Up: Your Clipboard Superpowers
Okay friend, let's recap what you've learned today.
The clipboard isn't just a "copy text" button. It's a powerful UX tool that can:
- ✅ Provide instant feedback to users
- ✅ Detect and validate content types
- ✅ Secure sensitive data with auto-clear
- ✅ Keep a history for power users
- ✅ Monitor changes for smart features
- ✅ Create seamless sharing experiences
And the best part? Most of this is super easy to implement with Riverpod!
The 5 Must-Have Features
Every app should have these:
- Visual Feedback: Users need to know when something is copied
- Smart Paste: Show users what they can paste
- Error Handling: Don't crash when clipboard fails
- Context Awareness: Different icons for different content types
- Secure Handling: Auto-clear sensitive data
The Impact
When you implement good clipboard UX:
- Users feel more confident
- Your app feels more polished
- Productivity increases
- Reviews improve
- Users notice the details
Small details like this separate good apps from great apps.
So go implement these patterns! Start with the basics (copy with feedback), then add the smart features (paste detection, history, secure handling) as you go.
Your users might not explicitly notice the clipboard improvements, but they'll definitely notice if it's done poorly. Do it right, and your app will feel premium.
Now go make your clipboard experience amazing! 📋✨
P.S. When you implement clipboard history and a user tells you "I love that feature!" — come back and tell me. That's my favorite feedback to hear! 🎉
P.P.S. Remember: Always clear sensitive data. Your users' security is more important than convenience. 🔐
Happy coding, and may your clipboards always be full of good stuff! 🚀
#Flutter #Clipboard #Riverpod #FlutterDev #CopyPaste #StateManagement #FlutterUI #UserExperience #MobileDevelopment #FlutterTutorial #FlutterBestPractices #ClipboardManagement #FlutterUX #SecureData #MobileApp #FlutterPatterns #AppDevelopment #FlutterWidget #DartLang #FlutterCommunity #UXDesign #ClipboardHistory #DataSecurity #FlutterGuide #MobileEngineering #AppUX #ProductivityFeatures #FlutterFeatures #DeveloperTools #CodeQuality
- Flutter Development
- User Experience
- Mobile Development
- State Management
- App Features
- flutter clipboard
- flutter copy paste
- riverpod clipboard
- flutter clipboard history
- secure clipboard flutter
- flutter paste detection
- clipboard state management
- flutter clipboard provider
- copy to clipboard flutter
- flutter clipboard tutorial
- clipboard ux flutter
- flutter clipboard listener
- flutter clipboard monitor
- smart paste flutter