【Flutter】Clipboardで簡単コピー&ペースト実装法

対象者

  • Flutterを用いたアプリ開発経験があり、機能拡張を目指している開発者
  • ユーザーエクスペリエンスを向上させるためのクリップボード操作に関心がある中級者
  • 効率的なコード実装とベストプラクティスを追求するプログラマ

はじめに

日常的なモバイルアプリ開発では、「テキストのコピー」や「ペースト」を実装するシーンに直面することが多々あります。ユーザーがアプリ内で生成したコンテンツを、簡単に他アプリへ持ち運んだり、逆に他アプリから取り込んだりするためにはクリップボード機能が欠かせません。本記事では、Flutterにおけるクリップボード操作を、シンプルな例を通して解説します。

クリップボード操作の基本

Flutterでクリップボードを扱うためには、package:flutter/services.dartを利用します。Clipboardクラスが用意されており、setDatagetDataメソッドを通じて、テキストなどをシステムクリップボードに書き込んだり、読み出したりすることが可能です。

コピー(setData)

ユーザーがボタンを押した際に任意のテキストをクリップボードへ書き込みます。

Clipboard.setData(const ClipboardData(text: 'コピーされたテキスト'));

右クリックのメニューから「コピー」する分には不要です。

ペースト(getData)

クリップボード上のテキストデータを取得します。

final data = await Clipboard.getData('text/plain');
setState(() {
    _copiedText = data?.text ?? 'データなし';
});

ユーザーへのフィードバック

コピー操作が成功した際にToastやSnackBarで「コピーしました」とユーザーに通知すると、操作性と信頼性が向上します。また、ペースト時には、過去にコピーしたテキストがない場合「データなし」と表示するなど、ユーザーが状況を理解しやすいようにすることが大切です。

HTMLのペースト

標準のClipboardでは、テキストとしてはペーストできましたが、HTMLをフォーマット付きではペーストできませんでした。そこでsuper_clipboardを使用したら、 HTMLをペーストできました。
PNG(画像全般?)でもペーストできるそうです。

super_clipboardのインストール

flutter pub add super_clipboard

htmlのペースト処理

final clipboard = SystemClipboard.instance;
if (clipboard == null) {
    return;
}

final reader = await clipboard.read();
if (reader.canProvide(Formats.htmlText)) {
    final html = await reader.readValue(Formats.htmlText);
    setState(() {
      if (html == null) {
        _copiedText = 'データなし';
      } else {
        _copiedText = html;
      }
    });
}

その他のパッケージ

試してませんが、以下のようなパッケージがありました。

クリップボードの中身を変更を検知する

まとめ

Flutterでのクリップボード操作は、Clipboardクラスを用いた単純なAPIコールで実現できます。しかし、単なるコピー&ペースト処理にとどまらず、ユーザーへのフィードバックを考慮することで、ユーザーが違和感なく情報を行き来でき、快適なユーザーエクスペリエンスの実現できます。
また標準外のパッケージでHTMLのペーストができました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:super_clipboard/super_clipboard.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _copiedText = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            FilledButton(
                onPressed: () {
                  Clipboard.setData(const ClipboardData(text: 'コピーされたテキスト'));
                },
                child: const Text('コピー')),
            FilledButton(
                onPressed: () async {
                  final data = await Clipboard.getData('text/plain');
                  setState(() {
                    _copiedText = data?.text ?? 'データなし';
                  });
                },
                child: const Text('ペースト')),
            FilledButton(
                onPressed: () async {
                  final clipboard = SystemClipboard.instance;
                  if (clipboard == null) {
                    return;
                  }
                  final reader = await clipboard.read();

                  if (reader.canProvide(Formats.htmlText)) {
                    final html = await reader.readValue(Formats.htmlText);
                    setState(() {
                      if (html == null) {
                        _copiedText = 'データなし';
                      } else {
                        _copiedText = html;
                      }
                    });
                  }
                },
                child: const Text('ペースト')),
            Text('コピーされたテキスト: $_copiedText'),
          ],
        ),
      ),
    );
  }
}