以前書いたこちらの記事、
ですが、その後
- Flutter で Mapbox の地図を操作してみる(Zenn)
- Flutter + Mapbox で Symbol をタップして id を取得・表示する(同上)
- Flutter + Mapbox でピン(Symbol)の情報を DB(SQLite)に保存する(同上)
を経て、
- 地図上にピンを立て、ピンについての情報を登録する機能
- ピンおよびピンについての情報は SQLite で DB 保存し、起動時に再読み込みする
- ピンに関連する写真を撮影してファイルとギャラリーに保存する機能
を追加しました。
- GitHub リポジトリ(hmatsu47 / maptool)
写真の撮影には image_picker を使いました。
【参考】
- FlutterでiOS、Android両方で動くカメラアプリを作る(okmt1230z さん)
- Flutterを使ったらカメラ機能が爆速で実装できた話(konatsu_p さん)
ただし、↑の頃と image_picker の仕様が若干変更されているので、こちらにメモとして残しておきます。
準備
主に以前の記事からの差分です。ただし写真撮影に関連のない部分は省略しています。
※記事執筆時点で Flutter SDK のバージョンは 2.5.2 です。
pubspec.yaml
(dependencies:
に追加)
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
内でインポートされているものを利用可)。
image_picker: ^0.8.4+4
image_gallery_saver: ^1.7.1
path_provider: ^2.0.7
Info.plist
(<dict>
〜</dict>
内に追加・iOS のみ)
<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>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
コード例(関連部分)
関連部分のみ一部改変して抽出しています。
※コード全体は**前掲の GitHub リポジトリ**をご確認ください。
パッケージインポート
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 0.8.2 より、一次ファイルとして取得する対象がFile
から cross_file のXFile
に変わりました。
なお、flutter_image_compress を使わなくても画像サイズや品質の編集が可能です。
※画像サイズの指定方法が flutter_image_compress とは異なる点に注意。
final ImagePicker _picker = ImagePicker();
(中略)
Future<XFile?> _takePhoto() {
return _picker.pickImage(
source: ImageSource.camera,
maxWidth: 1600,
maxHeight: 1600,
imageQuality: 85);
}
撮影した写真をファイルに保存する
事前に path_provider のgetApplicationDocumentsDirectory
で保存先のパスを取得・セットしておきます。
String _imagePath = '';
(中略)
_setImagePath() async {
_imagePath = (await getApplicationDocumentsDirectory()).path;
}
XFile.readAsBytes
で内容をUint8List
に読み出して、保存先のパスにFile.writeAsBytesSync
で書き出します。
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;
}
写真撮影→保存の呼び出し元はこのようになります。
final XFile? photo = await _takePhoto();
if (photo != null) {
await _savePhoto(photo);
}
iOS の場合、通常はアプリケーションで書き出したファイルをユーザが直接確認することはできませんが、先述のInfo.plist
の指定によって「ファイル」から確認できるようになります。
※SQLite の DB 保存ファイルも見えるので、うっかり削除しないよう注意。
【参考】
- Flutter 端末内のユーザーがアクセス可能な場所にファイルを保存する方法(バックアップ機能の実装)(halzo appdev blog)
保存したファイルを呼び出す
path_provider のgetApplicationDocumentsDirectory
で取得した保存先パスですが、これは仮想的なパスのようで、アプリケーションを起動するごとに変わる可能性があります。
ファイルの内容が消えるわけではありませんが、このパスは DB に直接保存して使うのではなく、アプリケーション機動時に取得し直して指定します。
もし DB にフルパスで保存している場合は、ファイルを読み取る際にパス部分を切り落として、ファイル名のみを新しい保存先パスに繋げて使用します。
// 画像の登録情報
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
になっているのは写真以外も共通して扱う想定だからです。