πŸ“· Implementing Clipboard Image Pasting in Flutter: A Cross-Platform Demo with PasteSnap

πŸ“· Implementing Clipboard Image Pasting in Flutter: A Cross-Platform Demo with PasteSnap

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!πŸš€

Struggling with image pasting in Flutter? See how I built a cross-platform demo using MethodChannel and BLoC for clipboard image pasting πŸš€

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 UIPasteboard in AppDelegate.swift.
  • Android: Access clipboard images using ClipboardManager in MainActivity.kt.
  • Flutter: Use MethodChannel to 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 _imageChannel is defined as a MethodChannel to 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 PasteImageEvent to the ChatBloc, which then handles the clipboard image retrieval via the MethodChannel.
  • TextField Context Menu: The contextMenuBuilder customizes TextField's context menu to include a Paste option, calling _handlePaste() when tapped.
  • Note: The hintText dynamically 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

Report Page