I can't figure out how to save to the camera roll (the equivalent of React Native's CameraRoll saveToCameraRoll().

  1. Flutter camera recommends using path_provider to get application directories, but it doesn't seem to have an option to get the camera roll directory path.

  2. I'm getting an exception on CameraController.capture

The relevant changes (and only the relevant changes, in the form of a diff) are here: https://gist.github.com/briankung/45f9d8438baab59ddcd3b6f3fe811d99

My whole main.dart is below for easy repro (search QUESTION: to find the relevant portions):

import 'dart:async';
import 'dart:io';

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

List<CameraDescription> cameras;

Future<Null> main() async {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]);

  cameras = await availableCameras();
  runApp(new CameraApp());
}

class CameraApp extends StatefulWidget {
  @override
  _CameraAppState createState() => new _CameraAppState();
}

class _CameraAppState extends State<CameraApp> {
  String _appDirectoryPath;
  CameraController controller;

  Future<void> _requestAppDirectory() async {
    // QUESTION: `path_provider` doesn't have getCameraRollDirectory()
    Directory _appDirectory = await getApplicationDocumentsDirectory();

    setState(() {
      _appDirectoryPath = _appDirectory.path;
    });
  }

  @override
  void initState() {
    super.initState();
    _requestAppDirectory();
    controller = new CameraController(cameras[0], ResolutionPreset.medium);
    controller.initialize().then((_) {
      if (!mounted) {
        return;
      }
      setState(() {});
    });
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (!controller.value.initialized) {
      return new Container();
    }

    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.red,
      ),
      home: new Scaffold(
        body: new Center(
          child: new AspectRatio(
            aspectRatio: controller.value.aspectRatio,
            child: new CameraPreview(controller),
          ),
        ),
        floatingActionButton: new FloatingActionButton(
          tooltip: 'Increment',
          child: new Icon(Icons.camera),
          onPressed: () {
            print('capturing');
            print(_appDirectoryPath);
            // QUESTION: this errors out
            controller.capture(_appDirectoryPath);
          },
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      ),
    );
  }
}

The log information is as follows:

I/flutter ( 5471): capturing
I/flutter ( 5471): /data/user/0/com.example.mycameraapp/app_flutter
W/LegacyRequestMapper( 5471): convertRequestMetadata - control.awbRegions setting is not supported, ignoring value
W/LegacyRequestMapper( 5471): Only received metering rectangles with weight 0.
W/LegacyRequestMapper( 5471): Only received metering rectangles with weight 0.
I/RequestThread-0( 5471): Received jpeg.
I/RequestThread-0( 5471): Producing jpeg buffer...
W/LegacyRequestMapper( 5471): convertRequestMetadata - control.awbRegions setting is not supported, ignoring value
W/LegacyRequestMapper( 5471): Only received metering rectangles with weight 0.
W/LegacyRequestMapper( 5471): Only received metering rectangles with weight 0.
E/flutter ( 5471): [ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
E/flutter ( 5471): CameraException(IOError, Failed saving image)
E/flutter ( 5471): #0      CameraController.capture (package:camera/camera.dart:234:7)
E/flutter ( 5471): <asynchronous suspension>
E/flutter ( 5471): #1      _CameraAppState.build.<anonymous closure> (file:///Users/briankung/workspace/mobile/flutter/my_camera_app/lib/main.dart:84:24)
E/flutter ( 5471): #2      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:478:14)
E/flutter ( 5471): #3      _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:530:30)
E/flutter ( 5471): #4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:102:24)
E/flutter ( 5471): #5      TapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:161:9)
E/flutter ( 5471): #6      TapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:123:7)
E/flutter ( 5471): #7      GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:156:27)
E/flutter ( 5471): #8      _WidgetsFlutterBinding&BindingBase&GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:147:20)
E/flutter ( 5471): #9      _WidgetsFlutterBinding&BindingBase&GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:121:22)
E/flutter ( 5471): #10     _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:101:7)
E/flutter ( 5471): #11     _WidgetsFlutterBinding&BindingBase&GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:64:7)
E/flutter ( 5471): #12     _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:48:7)
E/flutter ( 5471): #13     _invoke1 (dart:ui/hooks.dart:134:13)
E/flutter ( 5471): #14     _dispatchPointerDataPacket (dart:ui/hooks.dart:91:5)
I/RequestQueue( 5471): Repeating capture request cancelled.

Thanks!

Forgot version numbers:

$ flutter --version
Flutter 0.2.8 • channel beta • https://github.com/flutter/flutter.git
Framework • revision b397406561 (10 days ago) • 2018-04-02 13:53:20 -0700
Engine • revision c903c217a1
Tools • Dart 2.0.0-dev.43.0.flutter-52afcba357

// pubspec.yaml

camera:
  dependency: "direct main"
  description:
    name: camera
    url: "https://pub.dartlang.org"
  source: hosted
  version: "0.1.2"
path_provider:
  dependency: "direct main"
  description:
    name: path_provider
    url: "https://pub.dartlang.org"
  source: hosted
  version: "0.4.0"


Solution 1: rmtmckenzie

Unfortunately I don't think that flutter currently exposes this functionality.

Your best bet is probably to write a plugin or use Platform Channels to perform this. You could use a temporary directory as they do in this example, and then pass the path to android, where you would read the file and insert it into the gallery something like this:

MediaStore.Images.Media.insertImage(
      getContentResolver(), 
      yourBitmap, 
      yourTitle , 
      yourDescription
);`

You might get lucky if you create a feature request in one of the flutter repositories and someone decides that they will help you out by writing the plugin for you.


Solution 2: Kirollos Morkos

The error you're getting is because you are trying to save a photo to /data/user/0/com.example.mycameraapp/app_flutter which is a directory, not a file.

You can use the flutter_photokit package to save photos/videos to a user's camera roll/custom album on iOS. You would need to capture the photo to the device's temporary directory or application directory and then from there you can transfer the file to a user's camera roll. Relevant parts of example shown below:

// At the top
import 'package:flutter_photokit/flutter_photokit.dart';

// Function in _CameraAppState
void _captureAndSaveToCameraRoll() async {
  String outputFilePath = '$_appDirectoryPath/test.jpg';
  // Capture the photo to the app directory
  await controller.capture(outputFilePath);
  // Save the photo to the user's camera roll
  FlutterPhotokit.saveToCameraRoll(filePath: outputFilePath);
}

...

// In your build function
floatingActionButton: new FloatingActionButton(
  tooltip: 'Increment',
  child: new Icon(Icons.camera),
  onPressed: _captureAndSaveToCameraRoll,
)

Disclaimer: I am the author of this plugin.


Solution 3: jelenap

You can use gallery_saver from pub.dev/packages/gallery_saver which saves both video and images in gallery/photos both for Android & iOS.

You just need to provide it with a temporary path to a file or url, it saves both local files and those from network.

This is how it's used:

GallerySaver.saveVideo(String path);
GallerySaver.saveImage(String path);

Both functions return true in a case file was successfully saved, and false in any other way.

My team developed this plugin.