【Flutter】シェア機能を実装する

# はじめに
シェア機能を実装する機会があり、調べましたので、記事にしたいと思います。
具体的な機能としては、Widgetを画像にして、シェアする機能です。
それを調べる過程で、テキストだけをシェアする方法もわかったので、それも含めて書いていきます。

# シェアする方法
- [share_plus](https://pub.dev/packages/share_plus)というパッケージを使用する
（他にもパッケージはあるみたいですが、これがスタンダードで解説記事も1番多い）
- URLスキームを使用する
　アプリを呼び出す用のURLを[url_launcher](https://pub.dev/packages/url_launcher)で呼び出します。
- シェアするサービスが提供しているAPIを利用する
- MethodChannelを使用してネイティブの機能を使用する
 [share_plus](https://pub.dev/packages/share_plus)もネイティブの機能を呼び出しているのですが、特定のアプリの場合、実装を変えるなどカスタマイズしたい場合はこちらの方法

# share_plusを使う
OSの共有機能を利用するので、1番お手軽に、汎用性が高い共有機能を実装できます。
お手軽かつ、汎用性が高い分、特定のアプリに対してカスタマイズはできず、アプリによって、共有できる・できないの制限があります。
制限とは、共有する内容がテキストだけ、画像だけ、画像とテキストなど、共有内容によって動作が変わるアプリやそもそも共有先に選べないなどです。
例えば、画像とテキストの場合、以下のような制限がありました。
- Instagramでは共有に失敗します（画像だけなら問題ないです）
- Facebookには共有はできるがテキストが反映されない
- Youtubeは選択対象として現れない（画像とテキストだから？動画なら選べるのかも）

このパッケージを使い、共有機能を実装する際は、最初に、何(画像だけ、テキストだけとか）を、どのアプリに共有するかを決めておくことをオススメします。

## 使用するパッケージ
- [share_plus](https://pub.dev/packages/share_plus)
OSのシェア機能を使うため使用
- [screenshot](https://pub.dev/packages/screenshot)
Widgetを画像にするため使用

## テキストをシェア

テキストをシェアするのは簡単で、以下のように呼び出せばシェアできます。

```dart
import 'package:share_plus/share_plus.dart';

const text = "Shareします";
const subject = "Share先を選択";
Share.share(
  text,
  subject: subject,
);
```

textが共有先に送信する内容（Xならポストの本文）で、subjectが共有画面のタイトルになります。

![IMG_6494.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/b7a4020f-ba84-c5bb-7248-69c4c5275966.jpeg)

![IMG_6495.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/1cfc2ccd-f44b-0a39-03fd-d5446f764e4e.png)

:::note info
共有先は、端末にインストールされたアプリに依存します。
XやLINEをインストールしていなければ、共有先には出てきません。
:::

:::note info
テキストの共有の場合は、InstagramやTikTokなどの動画や画像が必要なアプリは共有先には出てきません。
:::


ちなみに、`subject`はnullableなので、指定しなくても問題ないです。
その場合は、`text`がタイトルになります。
```dart
import 'package:share_plus/share_plus.dart';

const text = "Shareします";
Share.share(
  text,
);
```

![IMG_6496.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/86632e87-f342-f0f0-61b2-016222671b11.jpeg)

## 画像をシェア

```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final ScreenshotController _screenShotController = ScreenshotController();

  @override
  Widget build(BuildContext context) {
    return Screenshot(
      controller: _screenShotController,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              // Widgetをキャプチャする
              final screenshot = await _screenShotController.capture(
                delay: const Duration(milliseconds: 500),
              );
              if (screenshot != null) {
                final shareFile =
                    XFile.fromData(screenshot, mimeType: "image/png");
                await Share.shareXFiles(
                  [
                    shareFile,
                  ],
                );
              }
            },
            child: const Text("Share"),
          ),
        ),
      ),
    );
  }

  Future<void> _saveImage({
    required String imagePath,
    required Uint8List imageData,
  }) async {
    final imageFile = await File(imagePath).create();
    await imageFile.writeAsBytes(imageData);
  }
}
```

### ポイント解説
#### Widgetを画像にする
以下の形で、childに指定したWidgetを画像にしています。

```dart
Screenshot(
  controller: _screenShotController,
  child: // 画像にするWidget,
)
```

実際にキャプチャするのは、下記の部分です。
`delay`を設定すると、設定した時間分キャプチャの時間をずらします。`delay`を設定しておかないと、即座にキャプチャされ、ボタンの押下のエフェクトが画像に表示されるので入れています。

```dart
// Widgetをキャプチャする
final screenshot = await _screenShotController.capture(
  delay: const Duration(milliseconds: 500));
```

#### 画像をShareする
画像をShareするのは下記の部分です。

```dart
final shareFile = XFile.fromData(screenshot, mimeType: "image/png");
await Share.shareXFiles(
  [
    shareFile,
  ],
);
```

画像だけじゃなく、textも設定可能です。

```dart
final shareFile = XFile.fromData(screenshot, mimeType: "image/png");
await Share.shareXFiles(
  [
    shareFile,
  ],
  text: "画像をShare！",
);
```

![IMG_6498.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/c7fe41cb-75d1-aa22-3fb7-46dd8806f726.png)

:::note alert
subjectを設定しても、下記のようにタイトルは変わらないみたいです。（androidは未確認）

![IMG_6499.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/6a5a3051-039c-75c9-7796-fe9a8b2b3e95.jpeg)

:::

:::note alert
Instagramの場合、textを設定すると、シェアできないみたいです。
Instagramには、複数枚の写真はシェアできない仕様らしく、textを設定していると、複数枚シェアしているという判定になるみたいです。

![IMG_6497.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/4d5d4a98-0cc2-9927-013c-9c77cb287c08.jpeg)
:::



# URLスキームを使用する
URLスキームを使用すると、特定のアプリケーションを起動して、特定のアクションを行うことができます。
例えば、Twitterを開き、Tweetしたい場合以下のようにします。

```Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    // 追加
	<key>LSApplicationQueriesSchemes</key>
	<array>
  		<string>twitter</string>
	</array>
</dict>
</plist>
```

```dart
import "package:url_launcher/url_launcher.dart";

final url = Uri.parse("twitter://post?message=Hello");
final canOpen = await canLaunchUrl(url);
if (!canOpen) {
  print("Twitter is not installed");
  return;
}

await launchUrl(url);
```

# シェアするサービスが提供しているAPIを利用する
サービスによっては、お金がかかったり、制限がありますが、シェア先に指定したいサービスのAPIを利用する方法です。
しかし、[share_plusを使う](#share_plusを使う)や[URLスキームを使用する](#URLスキームを使用する)方法とは異なり、シェア先のアプリを起動して操作して共有するわけではないので、選択肢として考えることはあまりなさそうです。

# MethodChannelを使用してネイティブの機能を使用する
ネイティブの機能を使用する方法で、下記の記事のように特定のアプリで共有したい内容などカスタマイズしたい場合はこちらの方法で実装する必要がありそうです。

https://mgre.co.jp/blog/10776