39
votes

I would like to share an image using the standard share dialogs in iOS and Android. The code below is mostly from https://pub.dartlang.org/packages/share which I'm using as a starting point (only Dart and Objective-C below). It currently shares only text.

Instead of Image below which I'm not sure is the best approach, how would I convert the image to a byte stream in Dart and handle in iOS and Android.

Dart

static const _kShareChannel = const MethodChannel('example.test.com/share');
Future<Null> shareImage(Image image) {
  assert(image != null);
  return _kShareChannel.invokeMethod('shareImage', image);
}

Objective-C

static NSString *const PLATFORM_CHANNEL = @"example.test.com/share";

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];

    FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;

    FlutterMethodChannel *shareChannel = [FlutterMethodChannel methodChannelWithName:PLATFORM_CHANNEL
                            binaryMessenger:controller];

    [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
        if ([@"shareImage" isEqualToString:call.method]) {
            [self share:call.arguments withController:[UIApplication sharedApplication].keyWindow.rootViewController];
            result(nil);
        } else {
            result([FlutterError errorWithCode:@"UNKNOWN_METHOD"
                                       message:@"Unknown share method called"
                                       details:nil]);
        }
    }];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)share:(id)sharedItems withController:(UIViewController *)controller {
    UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[ sharedItems ]
                                  applicationActivities:nil];
    [controller presentViewController:activityViewController animated:YES completion:nil];
}
6
@DuncanJones what do you need exactly ? - Rémi Rousselet
@RémiRousselet As a relative newcomer to Flutter, I didn't understand Collin's answer and wasn't sure how to proceed. So I guess I'm looking for a more detailed walk through of the tasks involved; where there might be a need for native code vs what can be achieved in Flutter, etc. - Duncan Jones
@DuncanJones does alardizabel's answer below help with your question? I guess that boils down to: what is the source of the image you want to share? The answer shows it coming from an asset. What's your use case? Captured from the camera? Downloaded over http? Generated in memory using a proprietary codec? Do you have an encoded image (jpeg, png, etc) or a raw raster that needs encoding, or something else? If the answer here doesn't help, perhaps ask your own question. Incidentally, does this help you with your vCard question? If not, another question... - Richard Heap
@RichardHeap The answer looks helpful (and will likely claim the bounty). In my specific case, the image I want to share is already a file in the apps document directory, but I'm sure I can figure out the remaining gaps myself. - Duncan Jones

6 Answers

45
votes

The below will allow you to send a file (specifically an image in this example) using UIActivityViewController on iOS and as a share intent on Android.

FileProvider overview (Android)

Update pubspec.yaml to reference your image if local (image.jpg in this example) and to use the path_provider plugin in order to access the file system. https://pub.dartlang.org/packages/path_provider

main.dart

import 'dart:io';
import 'dart:typed_data';

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

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Share Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Share Demo Home Page'),
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {

    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _shareImage,
        tooltip: 'Share',
        child: new Icon(Icons.share),
      ),
    );
  }

  _shareImage() async {
    try {
      final ByteData bytes = await rootBundle.load('assets/image.jpg');
      final Uint8List list = bytes.buffer.asUint8List();

      final tempDir = await getTemporaryDirectory();
      final file = await new File('${tempDir.path}/image.jpg').create();
      file.writeAsBytesSync(list);

      final channel = const MethodChannel('channel:me.albie.share/share');
      channel.invokeMethod('shareFile', 'image.jpg');

    } catch (e) {
      print('Share error: $e');
    }
  }
}

AppDelegate.m

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"

@implementation AppDelegate

static NSString *const SHARE_CHANNEL = @"channel:me.albie.share/share";

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];
    FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;

    FlutterMethodChannel *shareChannel =
    [FlutterMethodChannel methodChannelWithName:SHARE_CHANNEL
                                binaryMessenger:controller];

    [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
        if ([@"shareFile" isEqualToString:call.method]) {
            [self shareFile:call.arguments
             withController:[UIApplication sharedApplication].keyWindow.rootViewController];
        }
    }];

    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)shareFile:(id)sharedItems withController:(UIViewController *)controller {
    NSMutableString *filePath = [NSMutableString stringWithString:sharedItems];
    NSString *docsPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *imagePath = [docsPath stringByAppendingPathComponent:filePath];
    NSURL *imageUrl = [NSURL fileURLWithPath:imagePath];
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    UIImage *shareImage = [UIImage imageWithData:imageData];

    UIActivityViewController *activityViewController =
    [[UIActivityViewController alloc] initWithActivityItems:@[ shareImage ]
                                      applicationActivities:nil];
    [controller presentViewController:activityViewController animated:YES completion:nil];
}

@end

MainActivity.java

package com.example.share;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

import java.io.File;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;

import android.support.v4.content.FileProvider;

public class MainActivity extends FlutterActivity {

    private static final String SHARE_CHANNEL = "channel:me.albie.share/share";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);

        new MethodChannel(this.getFlutterView(), SHARE_CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            public final void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
                if (methodCall.method.equals("shareFile")) {
                    shareFile((String) methodCall.arguments);
                }
            }
        });
    }

    private void shareFile(String path) {
        File imageFile = new File(this.getApplicationContext().getCacheDir(), path);
        Uri contentUri = FileProvider.getUriForFile(this, "me.albie.share", imageFile);
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("image/jpg");
        shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
        this.startActivity(Intent.createChooser(shareIntent, "Share image using"));
    }
}

AndroidManifest.xml

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="me.albie.share"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path name="images" path="/"/>
</paths>

build.gradle (app)

dependencies {
    ...
    implementation 'com.android.support:support-v4:27.1.1'
}
13
votes

We put that functionality into a plugin: https://pub.dartlang.org/packages/esys_flutter_share.

Dart:

final ByteData bytes = await rootBundle.load('assets/image1.png');
await Share.file('esys image', 'esys.png', bytes.buffer.asUint8List(), 'image/png');
6
votes

Try using wc_flutter_share.

https://pub.dev/packages/wc_flutter_share

This plugin supports sharing image, text and subject. Unique thing about this plugin is that it also supports sharing image and text simultaneously which other plugins don't support at the time I am writing this answer.

5
votes

Thanks to @albert-lardizabal for the code above it works just perfect!! I had to translate it to Swift and Kotlin, so here's the code in case you guys need it:

Swift:

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
    ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)


    let shareChannelName = "channel:me.albie.share/share";
    let controller:FlutterViewController = self.window?.rootViewController as! FlutterViewController;
    let shareChannel:FlutterMethodChannel = FlutterMethodChannel.init(name: shareChannelName, binaryMessenger: controller);

    shareChannel.setMethodCallHandler({
        (call: FlutterMethodCall, result: FlutterResult) -> Void in
        if (call.method == "shareFile") {
            self.shareFile(sharedItems: call.arguments!,controller: controller);
        }
    });


    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func shareFile(sharedItems:Any, controller:UIViewController) {
    let filePath:NSMutableString = NSMutableString.init(string: sharedItems as! String);
    let docsPath:NSString = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]) as NSString;
    let imagePath = docsPath.appendingPathComponent(filePath as String);
    let imageUrl = URL.init(fileURLWithPath: imagePath, relativeTo: nil);
    do {
        let imageData = try Data.init(contentsOf: imageUrl);
        let shareImage = UIImage.init(data: imageData);
        let activityViewController:UIActivityViewController = UIActivityViewController.init(activityItems: [shareImage!], applicationActivities: nil);
        controller.present(activityViewController, animated: true, completion: nil);
    } catch let error {
        print(error.localizedDescription);
    }
}

Kotlin:

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    MethodChannel(flutterView,"channel:me.albie.share/share").setMethodCallHandler { methodCall, _ ->
        if (methodCall.method == "shareFile") {
            shareFile(methodCall.arguments as String)
        }
    }
}

private fun shareFile(path:String) {
    val imageFile = File(this.applicationContext.cacheDir,path)
    val contentUri = FileProvider.getUriForFile(this,"me.albie.share",imageFile)

    val shareIntent = Intent()
    shareIntent.action = Intent.ACTION_SEND
    shareIntent.type="image/jpg"
    shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
    startActivity(Intent.createChooser(shareIntent,"Compartir usando"))
}
1
votes

If the image file is downloaded, I would recommend saving it to a temporary file in Dart.

await new File('${systemTempDir.path}/foo.jpg').create();

Then you can invoke UIActivityViewController with a URL representing the filename of the image file. Here's some sample code on how to do this in a non-Flutter app that should get you started.

If your image file is constructed dynamically (e.g. using the Canvas API), you may be wondering how to encode a ui.Image object into an image file. The Flutter engine doesn't currently provide a way to do that, it would be possible modify the engine to add that support. You can take a look at how screenshot support is implemented for inspiration, but it won't be trivial.

0
votes

I would suggest using the following flutter plugin:

https://pub.dartlang.org/packages/share

Text sharing is quite simple:

Share.share('Text I wish to share');

For image: Better convert an image to Base64 string and send as a string.