Introduction
Flutter is great for cross-platform apps, but its Clipboard class only supports text, not images. For a project, I needed users to paste images into a chat interface โ a common feature in messaging apps. Since Flutter lacks native support, I used MethodChannel to integrate platform-specific code for iOS and Android. To showcase this, I built PasteSnap, a demo app focused on clipboard image pasting in a chat-like interface.
The Problem: Flutter's Text-Only Clipboard
Flutter's Clipboard class (from package:flutter/services) handles text well but doesn't support images. I needed to enable image pasting in a chat interface, like in WhatsApp, which Flutter can't do natively. I solved this by using platform-specific APIs on iOS and Android, connected to Flutter via MethodChannel. To demonstrate this without exposing my main project, I created PasteSnap โ a minimal app focused on clipboard image pasting.
The Solution: Native Integration with MethodChannel
The approach involves three key components:
- iOS: Retrieve clipboard images using
UIPasteboardin AppDelegate.swift. - Android: Access clipboard images using
ClipboardManagerin MainActivity.kt. - Flutter: Use
MethodChannelto communicate between the native layer and the Dart layer, integrating the feature into a ChatBloc and ChatScreen.
This cross-platform solution ensures the feature works seamlessly on both iOS and Android, as demonstrated in PasteSnap.
Step-by-Step Implementation
Setting Up the Flutter Project
I set up a Flutter project with the following dependencies in pubspec.yaml. These packages provide the tools needed for state management and file handling.
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.0
equatable: ^2.0.0
path_provider: ^2.1.5- flutter_bloc and equatable to enable the BLoC pattern for state management.
- path_provider assists with file system access (though not directly used in the snippets here, it's part of the full project).
Step 1: Define the MethodChannel in Flutter
In your Flutter project, define a MethodChannel in the ChatBloc class. This channel will be used to communicate with the native code to retrieve images from the clipboard.
import 'dart:typed_data';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/services.dart';
part 'chat_bloc_event.dart';
part 'chat_bloc_state.dart';
class ChatBloc extends Bloc<ChatEvent, ChatState> {
static const _imageChannel = MethodChannel('clipboard/image');
ChatBloc() : super(const ChatState(text: '')) {
on<PasteImageEvent>(_onPasteImage);
on<UpdateTextEvent>(_onUpdateText);
on<SendMessageEvent>(_onSendMessage);
}
Future<void> _onPasteImage(PasteImageEvent event, Emitter<ChatState> emit) async {
try {
final imageData = await _imageChannel.invokeMethod('getClipboardImage');
if (imageData != null && imageData is Uint8List) {
emit(ChatState(
text: state.text,
previewImage: imageData,
messages: state.messages,
));
} else {
print('No image data found in clipboard');
}
} catch (e) {
print('Failed to get clipboard image: $e');
}
}
}Explanation:
- The
_imageChannelis defined as aMethodChannelto communicate with the native layer. - In
_onPasteImage, it invokesgetClipboardImage, expecting a Uint8List (image data). If successful, it updates the previewImage in the state.
Step 2: iOS Implementation (AppDelegate.swift)
In AppDelegate.swift, set up a method call handler to listen for method calls from Flutter. When the getClipboardImage method is called, retrieving the image from the clipboard and returning it as a byte array.
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let imageChannel = FlutterMethodChannel(name: "clipboard/image",
binaryMessenger: controller.binaryMessenger)
imageChannel.setMethodCallHandler { [weak self] (call, result) in
if call.method == "getClipboardImage" {
self?.getClipboardImage(result: result)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getClipboardImage(result: FlutterResult) {
if let image = UIPasteboard.general.image,
let data = image.jpegData(compressionQuality: 0.9) {
result(data)
} else {
result(nil)
}
}
}Explanation:
- UIPasteboard.general.image checks if the clipboard contains an image.
- If an image is found, it's converted to JPEG data with a compression quality of 0.9 to reduce size.
- The byte array (Data) is returned to Flutter via the result. If no image is present, nil is returned.
Step 3: Android Implementation (MainActivity.kt)
In MainActivity.kt, set up a method call handler to listen for method calls from Flutter. When the getClipboardImage method is called, retrieving the image from the clipboard and returning it as a byte array.
package com.example.paste_snap_demo
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.ByteArrayOutputStream
class MainActivity : FlutterActivity() {
private val CHANNEL = "clipboard/image"
override fun configureFlutterEngine(flutterEngine: io.flutter.embedding.engine.FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getClipboardImage") {
val imageBytes = getClipboardImage()
if (imageBytes != null) {
result.success(imageBytes)
} else {
result.success(null)
}
} else {
result.notImplemented()
}
}
}
private fun getClipboardImage(): ByteArray? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
if (!clipboard.hasPrimaryClip()) {
return null
}
val clipData = clipboard.primaryClip
if (clipData != null && clipData.itemCount > 0) {
val item = clipData.getItemAt(0)
val uri = item.uri
if (uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream)
inputStream?.close()
if (bitmap != null) {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
val byteArray = stream.toByteArray()
bitmap.recycle()
stream.close()
return byteArray
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
return null
}
}Explanation:
- ClipboardManager checks if the clipboard has a primary clip.
- If a clip with a URI is found, it's converted to a Bitmap using the contentResolver.
- The bitmap is compressed to JPEG with 90% quality and returned as a byte array. If no image is present, null is returned.
Step 4: Update the Flutter UI
In chat_screen.dart, update the UI to handle pasting images through a custom context menu in the TextField, and the _handlePaste method that triggers the image paste action. Here's the relevant UI part:
// Method to handle the pasting mechanism
void _handlePaste() {
context.read<ChatBloc>().add(PasteImageEvent());
}
// TextField with custom context menu for pasting
TextField(
controller: _messageController,
decoration: const InputDecoration(
hintText: 'Type a message...',
border: InputBorder.none,
hintStyle: TextStyle(
color: PasteSnapColors.textSecondary,
fontSize: 15,
),
contentPadding: EdgeInsets.symmetric(vertical: 10),
),
maxLines: 5,
minLines: 1,
style: const TextStyle(
color: PasteSnapColors.textPrimary,
fontSize: 15,
),
textCapitalization: TextCapitalization.sentences,
contextMenuBuilder: (context, editableTextState) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: [
ContextMenuButtonItem(
label: 'Paste',
onPressed: () {
ContextMenuController.removeAny();
_handlePaste();
},
),
],
);
},
),Explanation:
- _handlePaste Method: Dispatches a
PasteImageEventto the ChatBloc, which then handles the clipboard image retrieval via theMethodChannel. - TextField Context Menu: The contextMenuBuilder customizes TextField's context menu to include a
Pasteoption, calling_handlePaste()when tapped. - Note: The
hintTextdynamically changes based on whether a preview image exists (not shown here for brevity but available on GitHub).
Visualizing the Result
Check out PasteSnap in action:

Conclusion: We Did It! ๐ Here We Go! ๐
Implementing clipboard image pasting in Flutter using MethodChannel and BLoC was a fun challenge. PasteSnap shows how this feature can enhance apps with image-sharing needs, all while keeping the architecture clean. Check out the full code on GitHub repository , try it out, and let me know your thoughts in the comments โ I can't wait to hear from you! ๐ฌ
Code Repository
Explore the complete PasteSnap codebase on GitHub: PasteSnap GitHub