💽

【Flutter】Widgetを画像化→SNSシェア機能を実装する

2021/05/17に公開2

はじめに

  1. 特定のWidgetを画像化し
  2. SNSにシェア

する流れを書いていきます。
なお、SNSへのシェアに関してはpackageのshare_extendを利用します。

背景

個人開発しているアプリで画像のようなWidgetを画像化→共有する機能を実装しました。

その際に、以下の2点で躓きました。

  • カメラやライブラリから画像をシェアすることは上であげたshare_extendだけで実装が可能。
    しかし、Widgetを画像化したものをそのまま直接シェアすることはできない。(一度ライブラリに保存すればシェア可能ではある。)
  • flutter_esys_shareというpackageを使えば画像化したものを直接シェア可能だが、2019年から更新がとまっており心許ない。

つまり、share_extendを利用しつつ、ライブラリに一度保存することなく直接画像をシェアする方法を実現する必要がありました。

ポイント

この機能の肝は
getApplicationDocumentsFile

こちらを利用することで要件を実現します。
ドキュメントはこちら。

事前準備

例としてこの赤いカードをシェアすることとします。

シェアするWidget

コードはこちら。

main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WidgetShareWidget(),
    );
  }
}

class WidgetShareWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SizedBox(
            height: 200,
            width: 300,
            child: Card(
              color: Colors.red,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  Text(
                    'Share this card!',
                    style: TextStyle(color: Colors.white),
                  ),
                  ElevatedButton(
                    onPressed: () => print('share'),
                    child: Text('Share'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

シェアする際に利用するパッケージshare_extendとパスを扱うためにpath_providerをインストールします。
pubspec.yamlに以下のコードを追加してpub get。

pubspec.yaml
share_extend: ^2.0.0
path_provider: ^2.0.1

事前準備はこれだけ。

1. Widgetを画像化する

画像化にはこちらの記事を参考にしました。
FlutterでWidgetを画像にしてSNS等にシェアする方法

  1. 画像するWidgetをRepaintBoundaryで囲み、keyをつける。
main.dart
class WidgetShareWidget extends StatelessWidget {
  + final GlobalKey shareKey = GlobalKey();//追加
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SizedBox(
            height: 200,
            width: 300,
            child: RepaintBoundary(
             + key: shareKey,//追加
              child: Card(...),
            ),
          ),
        ),
      ),
    );
  }
}
  1. Widgetを画像化する。
    変換自体はあっさりできます。
    下のコードでは後々に備えてbytedataとして出力しています。
main.dart
import 'dart:ui' as ui;

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

Future<ByteData> exportToImage(GlobalKey globalKey) async {
    final boundary =
        globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
    final image = await boundary.toImage(
      pixelRatio: 3,
    );
    final byteData = await image.toByteData(
      format: ui.ImageByteFormat.png,
    );
    return byteData!;
  }

2. 画像をSNSにシェアする

まず、share_extendを確認します。
画像のシェアするためにはドキュメントにある通り、画像のあるディレクトリへのパスが必要となります。

Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture');
Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']);

以上を踏まえ、以下の流れで実装していきます。

  1. 作成した画像をアプリ内のディレクトリへ保存しパスを取得。
  2. ディレクトへのパスを取得し、シェアする。

1. 作成した画像をアプリ内のディレクトリへ保存しパスを取得。

ここがポイント。
アプリ内のディレクトリにアクセスし画像を保存します。
こちらのドキュメントに書いてある通り、

path_provider
dart:io

を用いることでアクセスができます。

In some cases, you need to read and write files to disk. For example, you may need to persist data across app launches, or download data from the internet and save it for later offline use. To save files to disk, combine the path_provider plugin with the dart:io library.

The path_provider package provides a platform-agnostic way to access commonly used locations on the device’s file system. The plugin currently supports access to two file system locations:

アクセスするディレクトリは

getApplicationDocumentsDirectory()

となります。

A temporary directory (cache) that the system can clear at any time. On iOS, this corresponds to the NSCachesDirectory. On Android, this is the value that getCacheDir() returns.

Future<String> get _localPath async {
  final directory = await getApplicationDocumentsDirectory();

  return directory.path;
}

こちらをもとに実装します。

import 'dart:io';
import 'package:path_provider/path_provider.dart';

 Future<File> getApplicationDocumentsFile(String text, List<int> imageData) async {
    final directory = await getApplicationDocumentsDirectory();

    final exportFile = File('${directory.path}/$text.png');
    if (!await exportFile.exists()) {
      await exportFile.create(recursive: true);
    }
    final file = await exportFile.writeAsBytes(imageData);
    return file;
  }

writeAsBytesを使って先ほど作成したbyteDataをUint8Listの状態でデイスレクトリに保存しています。

2. ディレクトへのパスを取得し、シェアする。

最後に先ほど保存したディレクトリへのパスを取得し、share_extendを利用してシェアします。

  void shareImageAndText(String text, GlobalKey globalKey) async {
    //shareする際のテキスト
    try {
      final bytes = await exportToImage(globalKey);
      //byte data→Uint8List
      final widgetImageBytes =
          bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes);
      //App directoryファイルに保存
      final applicationDocumentsFile =
          await getApplicationDocumentsFile(text, widgetImageBytes);

      final path = applicationDocumentsFile.path;
      await ShareExtend.share(path, "image");
      applicationDocumentsFile.delete();
    } catch (error) {
      print(error);
    }
  }

シェア完了後、

applicationDocumentsFile.delete();

を使ってファイルが無駄にたまらないようにしています。
(これはgetApplicationDocumentsDirectoryの場合はいらないのかもですが念のため入れています。)

完成コード

完成したコードはこちら

main.dart
import 'dart:io';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_extend/share_extend.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WidgetShareWidget(),
    );
  }
}

class WidgetShareWidget extends StatelessWidget {
  final GlobalKey shareKey = GlobalKey();

  Future<ByteData> exportToImage(GlobalKey globalKey) async {
    final boundary =
        globalKey.currentContext.findRenderObject() as RenderRepaintBoundary;
    final image = await boundary.toImage(
      pixelRatio: 3,
    );
    final byteData = await image.toByteData(
      format: ui.ImageByteFormat.png,
    );
    return byteData;
  }

  Future<File> getApplicationDocumentsFile(
      String text, List<int> imageData) async {
    final directory = await getApplicationDocumentsDirectory();

    final exportFile = File('${directory.path}/$text.png');
    if (!await exportFile.exists()) {
      await exportFile.create(recursive: true);
    }
    final file = await exportFile.writeAsBytes(imageData);
    return file;
  }

  void shareImageAndText(String text, GlobalKey globalKey) async {
    //shareする際のテキスト
    try {
      //byte dataに
      final bytes = await exportToImage(globalKey);
      final widgetImageData =
          bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes);
      //App directoryファイルに保存
      final applicationDocumentsFile =
          await getApplicationDocumentsFile(text, widgetImageData);

      final path = applicationDocumentsFile.path;
      await ShareExtend.share(path, "image");
      applicationDocumentsFile.delete();
    } catch (error) {
      print(error);
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SizedBox(
            height: 200,
            width: 300,
            child: RepaintBoundary(
              key: shareKey,
              child: Card(
                color: Colors.red,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    Text(
                      'Share this card!',
                      style: TextStyle(color: Colors.white),
                    ),
                    ElevatedButton(
                      onPressed: () => shareImageAndText(
                        'sample_widget',
                        shareKey,
                      ),
                      child: Text('Share'),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

最後に

アプリ内のディレクトリへのアクセスを学べたことで開発の幅が広がりそうです!
ただ、理解がざっくりであやふやな部分も多いので誤っている点などあればぜひコメントください。

個人開発アプリ

自分はエンジニアではないですが個人開発を趣味としていて、BIG3記録管理アプリ「LifterLog」 をリリース中です。

FlutterとFirebase, Cloud Functionsで開発しています。
パワーリフティングに興味ないと全くわけわからんアプリですが、よければ一度触ってみてください!

Discussion

tama8021tama8021

参考になりました!ありがとうございます!

それで参考にしながら試してみたんですけど、シェアするWidget内にシェアボタンがあると上手く動かないみたいです。

applicationDocumentsFile.delete();のコード消したらいけました!

https://github.com/flutter/flutter/issues/22308

RereRere

シェアするWidget内にシェアボタンがあると上手く動かないみたいです

情報ありがとうございます。
確認して修正しておきます!