Flutter で Image Picker（0.8.2 以降）を使って撮った写真をファイル保存する

以前書いたこちらの記事、

 - **[Flutter で Mapbox の地図を現在位置を中心にして表示してみる](https://qiita.com/hmatsu47/items/b98ef4c1a87cc0ec415d)**

ですが、その後

 - **[Flutter で Mapbox の地図を操作してみる](https://zenn.dev/hmatsu47/articles/846c3186f5b4fe)（Zenn）**
 - **[Flutter + Mapbox で Symbol をタップして id を取得・表示する](https://zenn.dev/hmatsu47/articles/9102fb79a99a98)（同上）**
 - **[Flutter + Mapbox でピン（Symbol）の情報を DB（SQLite）に保存する](https://zenn.dev/hmatsu47/articles/e81bf3c2bf00f8)（同上）**

を経て、

 - 地図上にピンを立て、ピンについての情報を登録する機能
   - ピンおよびピンについての情報は SQLite で DB 保存し、起動時に再読み込みする
 - ピンに関連する写真を撮影してファイルとギャラリーに保存する機能

を追加しました。

 - **[GitHub リポジトリ](https://github.com/hmatsu47/maptool)（hmatsu47 / maptool）**

写真の撮影には **[image_picker](https://pub.dev/packages/image_picker)** を使いました。

**【参考】**

 - **[FlutterでiOS、Android両方で動くカメラアプリを作る](https://qiita.com/okmt1230z/items/321f7fa2b49999c59de9)（okmt1230z さん）**
 - **[Flutterを使ったらカメラ機能が爆速で実装できた話](https://qiita.com/konatsu_p/items/eb0e8a7ab62ab9d31315)（konatsu_p さん）**

ただし、↑の頃と **[image_picker](https://pub.dev/packages/image_picker)** の仕様が若干変更されているので、こちらにメモとして残しておきます。

# 準備

主に以前の記事からの差分です。ただし写真撮影に関連のない部分は省略しています。

※記事執筆時点で Flutter SDK のバージョンは 2.5.2 です。

 - **`pubspec.yaml`（`dependencies:`に追加）**

```yaml:pubspec.yaml（部分）
  image_picker: ^0.8.4+2
  cross_file: ^0.3.1+5
  image_gallery_saver: ^1.7.0
  path_provider: ^2.0.5
```

---

**2021/12/09 追記：**
Flutter 2.8（Dart 2.15）の環境では`cross_file`の明示的な指定が不要になりました（`image_picker`内でインポートされているものを利用可）。

```yaml:pubspec.yaml（部分）
  image_picker: ^0.8.4+4
  image_gallery_saver: ^1.7.1
  path_provider: ^2.0.7
```

---

 - **`Info.plist`（`<dict>`〜`</dict>`内に追加・iOS のみ）**

```xml:Info.plist（image_picker関連）
	<key>NSPhotoLibraryUsageDescription</key>
	<string>This app requires to access your photo library</string>
	<key>NSCameraUsageDescription</key>
	<string>This app requires to add file to your camera</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>This app requires to add file to your photo library your microphone</string>
```

```xml:Info.plist（path_providerファイル保存先関連）
	<key>UISupportsDocumentBrowser</key>
	<true/>
	<key>LSSupportsOpeningDocumentsInPlace</key>
	<true/>
```

# コード例（関連部分）

関連部分のみ一部改変して抽出しています。

※コード全体は**[前掲の GitHub リポジトリ](https://github.com/hmatsu47/maptool)**をご確認ください。

## パッケージインポート

```dart:パッケージインポート
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
```

---

**2021/12/09 追記：**
前述のとおりですが、Flutter 2.8（Dart 2.15）の環境では`cross_file`のインポートが不要になりました。

---

## 写真を撮影する

**[image_picker](https://pub.dev/packages/image_picker)** 0.8.2 より、一次ファイルとして取得する対象が[`File`](https://api.flutter.dev/flutter/dart-io/File-class.html)から **[cross_file](https://pub.dev/packages/cross_file)** の[`XFile`](https://pub.dev/documentation/cross_file/latest/cross_file/XFile-class.html)に変わりました。

なお、**[flutter_image_compress](https://pub.dev/packages/flutter_image_compress)** を使わなくても画像サイズや品質の編集が可能です。

※画像サイズの指定方法が　**[flutter_image_compress](https://pub.dev/packages/flutter_image_compress)** とは異なる点に注意。

```dart:写真撮影
  final ImagePicker _picker = ImagePicker();
（中略）
  Future<XFile?> _takePhoto() {
    return _picker.pickImage(
        source: ImageSource.camera,
        maxWidth: 1600,
        maxHeight: 1600,
        imageQuality: 85);
  }
```

## 撮影した写真をファイルに保存する

事前に　**[path_provider](https://pub.dev/packages/path_provider)** の[`getApplicationDocumentsDirectory`](https://pub.dev/documentation/path_provider/latest/path_provider/getApplicationDocumentsDirectory.html)で保存先のパスを取得・セットしておきます。

```dart:保存先パスをセット
  String _imagePath = '';
（中略）
  _setImagePath() async {
    _imagePath = (await getApplicationDocumentsDirectory()).path;
  }
```

[`XFile.readAsBytes`](https://pub.dev/documentation/cross_file/latest/cross_file/XFile/readAsBytes.html)で内容を[`Uint8List`](https://api.flutter.dev/flutter/dart-typed_data/Uint8List-class.html)に読み出して、保存先のパスに[`File.writeAsBytesSync`](https://api.flutter.dev/flutter/dart-io/File/writeAsBytesSync.html)で書き出します。

```dart:写真を保存する
  Future<String> _savePhoto(XFile photo) async {
    final Uint8List buffer = await photo.readAsBytes();
    final String savePath = '$_imagePath/${photo.name}';
    final File saveFile = File(savePath);
    saveFile.writeAsBytesSync(buffer, flush: true, mode: FileMode.write);
    // 画像ギャラリーにも保存（オプション）
    await ImageGallerySaver.saveImage(buffer, name: photo.name);

    return saveFile.path;
  }
```

写真撮影→保存の呼び出し元はこのようになります。

```dart:呼び出し元
  final XFile? photo = await _takePhoto();
  if (photo != null) {
    await _savePhoto(photo);
  }
```

iOS の場合、通常はアプリケーションで書き出したファイルをユーザが直接確認することはできませんが、先述の`Info.plist`の指定によって「ファイル」から確認できるようになります。

※SQLite の DB 保存ファイルも見えるので、うっかり削除しないよう注意。

**【参考】**

 - **[Flutter　 端末内のユーザーがアクセス可能な場所にファイルを保存する方法（バックアップ機能の実装）](https://halzoblog.com/flutter-backup-in-device/)（halzo appdev blog）**

## 保存したファイルを呼び出す

　**[path_provider](https://pub.dev/packages/path_provider)** の[`getApplicationDocumentsDirectory`](https://pub.dev/documentation/path_provider/latest/path_provider/getApplicationDocumentsDirectory.html)で取得した保存先パスですが、これは仮想的なパスのようで、アプリケーションを起動するごとに変わる可能性があります。

ファイルの内容が消えるわけではありませんが、このパスは　DB に直接保存して使うのではなく、アプリケーション機動時に取得し直して指定します。

もし DB にフルパスで保存している場合は、ファイルを読み取る際にパス部分を切り落として、ファイル名のみを新しい保存先パスに繋げて使用します。

```dart:画像ファイル取得
// 画像の登録情報
class Picture {
  int id;
  String comment;
  DateTime dateTime;
  String filePath;

  Picture(this.id, this.comment, this.dateTime, this.filePath);
}
（中略）
  // 画像ファイル取得
  File? _localFile(Picture picture) {
    // filePath がパス付きの場合はファイル名のみを抽出
    final int pathIndexOf = picture.filePath.lastIndexOf('/');
    final String fileName = (pathIndexOf == -1
        ? picture.filePath
        : picture.filePath.substring(pathIndexOf + 1));
    final String filePath = '$_imagePath/$fileName';
    try {
      if (File(filePath).existsSync()) {
        return File(filePath);
      }
      return null;
    } catch (e) {
      return null;
    }
  }
```

これを`File? file = _localFile(picture);`→`Image.file(file)`などで表示します。

※余談ですが、ここだけ`Photo`ではなく`Picture`になっているのは写真以外も共通して扱う想定だからです。