[Flutter][クソアプリ工房][002] ゴミマップ ④ スマホのカメラで写真撮影して画像を保存する

前回、 [Flutter][クソアプリ工房][002] ゴミマップ ③ スマホの内蔵カメラで写真撮影できるようにする の続き。

『ゴミマップ』の開発を進めていきます。

 

はじめに

  • 音声付きの解説動画をYouTube『クソアプリ工房』にアップしています。(ゴミマップで検索)
  • 文字を読むのがダルい、聞き流しで Flutter 開発勉強できないかなーとお考えの方はぜひ覗いてみてください。
  • 本アプリのソースコードは全て無料でGitHubに公開しております。

ちなみに、私はコミュ障ぼっちでフリーランスのエンジニアをやっています、34歳です。私のプロフィールに興味を持ってくださった方は、ページ下部の筆者紹介、Twitter(@suekiaoi)やInstagram(@aoi.sueki)などからご確認いただければと思います。

また、ぼっち社会人向けに1年かけて書いていたコラムが書籍化されました。ぜひ一度、お手にとって読んでみてください。

友達0のコミュ障が「一人」で稼げるようになったぼっち仕事術 (アルファポリス)

 

※※※ここから先、迷走&エラー対処が続きます。

とにかく自分のアプリ開発の参考にしたいだけ、という方は、最初から以下を参考にしてもらえらばと思います。

[公式] Flutter の camera plugin に関して参考にした記事

Take a picture using the cameraHow to use a camera plugin on mobile.
Take a picture using the camera flutter.dev
Take a picture using the camera

このソースコード、ほぼそのまま使えます。ありがたや。つか先に気づけば良かった。。。

 

 

ゴール:『ゴミマップ』画面イメージ

①で作ったこのイメージに従って実装していきます。

ちなみに『ゴミマップ』機能は超シンプル。

  • Googleマップを表示する
  • 現在地を表示する
  • 写真を撮る
  • マップ上に写真を表示する
  • 写真が撮影された座標を登録/削除する

 

〜ここまでの流れ〜

STEP1 [クソアプリ工房][002] ゴミマップ ①企画・設計

STEP2 [Flutter][クソアプリ工房][002] ゴミマップ ② GoogleMapとlocationを使ってアプリ上に地図を表示する

STEP3 [Flutter][クソアプリ工房][002] ゴミマップ ③ スマホの内蔵カメラで写真撮影できるようにする

 

STEP4 Camera Plugin を使って写真撮影できるようにする

前回、スマホの内臓カメラで写真撮影できるかどうか、テストする直前までいきました。

実は、先に言っておくと写真撮影しようとしたらエラーになりました。どうやら、Camera Plugin を使わなきゃいけないようです。

 

遭遇したエラー Error Camera not available.

とりあえずこの状態でシミュレータの撮影ボタン押したらエラーになりました。反応が返ってきて嬉しい。

Error Camera not available.

<解決方法>

Flutter には 内蔵カメラを使うためのインターフェースとして camera plugin というパッケージがあるみたい。

 

手順① Camera Plugin をインストール

camera | Flutter PackageA Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dar
camera | Flutter Package pub.dev
camera | Flutter Package

インストールしてExampleを参考にmainも書き換えてみる。

lib/main.dart

import 'package:camera/camera.dart';


List<CameraDescription> cameras;

Future<void> main() async {
  cameras = await availableCameras();
  runApp(MyApp());
}
...
class _MyHomePageState extends State<MyHomePage> {

  CameraController _cameraController;
  File _image;
  final picker = ImagePicker();

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

  Future getImage() async {
    var image = await ImagePicker.pickImage(source: ImageSource.camera);
    setState(() {
      _image = image;
      print(_image.toString());
    });
    Navigator.of(context).pop();
  }
...
  @override
  void initState() {
    super.initState();

    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();

    _loading = true;

    initPlatformState();
    _locationService.onLocationChanged.listen((LocationData result) async {
      setState(() {
        currentLocation = result;
      });
    });

    _cameraController = CameraController(cameras[0], ResolutionPreset.max);
    _cameraController.initialize().then((_) {
      if (!mounted) {
        return;
      }
      setState(() {});
    });
  }

 

遭遇したエラー ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized

アプリは起動せず、画面真っ白のまま落ちちゃいました。

以下がコンソールのエラーメッセージ。

Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before `runApp()` has been called (for example, during plugin initialization), then you need to explicitly call the `WidgetsFlutterBinding.ensureInitialized()` first.
If you're running a test, you can call the `TestWidgetsFlutterBinding.ensureInitialized()` as the first line in your test's `main()` method to initialize the binding.も

日本語訳すると、

未処理の例外:バインディングが初期化される前に、ServicesBinding.defaultBinaryMessengerにアクセスしました。
アプリケーションを実行していて、 `runApp()`が呼び出される前に(たとえば、プラグインの初期化中に)バイナリメッセンジャーにアクセスする必要がある場合は、最初に `WidgetsFlutterBinding.ensureInitialized()`を明示的に呼び出す必要があります。
テストを実行している場合は、テストの `main()`メソッドの最初の行として `TestWidgetsFlutterBinding.ensureInitialized()`を呼び出して、バインディングを初期化できます。

 

…cameraを召喚するには呪文が足りないよってことですね!

とりあえず親切に教えてくれている通りにやってみましょう。

 

<解決方法>

以下を main() に追記。

lib/main.dart

Future<void> main() async{

  WidgetsFlutterBinding.ensureInitialized();

  cameras = await availableCameras();
  runApp(MyApp());
}

 

これでOK!

厳密には次のエラーメッセージが出ましたが、このエラーに関しては解決、という意味で。

 

遭遇したエラー RangeError (index): Invalid value: Valid value range is empty: 0

RangeError (index): Invalid value: Valid value range is empty: 0

なんということでしょう。以下で取得したはずのcameras に何も入っていません。

await availableCameras();

そのため、cameras[0]で camerasのindex=0の要素を取ろうとして、そんなもんないけど? ってエラーになっています。

_cameraController = CameraController(cameras[0], ResolutionPreset.max); ←ここ
_cameraController.initialize().then((_) {

 

cameras[0]ではなく、最近のコードだと cameras.first と書いているのが多いので、そのように修正してみる。

遭遇したエラー Bad state: No element

Bad state: No element

ふん、ダメっすね。camerasが空なんだから、当たり前か。

 

<解決方法>

先ほど、cameras は main内に 以下のように書いていましたが、よくみたら MyApp にcamerasを渡していませんね。

 

lib/main.dart

// 使用できるカメラのリストを取得
Future main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final cameras = await availableCameras();
  runApp(
    MaterialApp(theme: ThemeData.dark(), home: MyApp(cameras: cameras)),
  );
}
...

class MyApp extends StatelessWidget {
final List<CameraDescription> cameras;
const MyApp({Key key, @required this.cameras}) : super(key: key);
@override Widget build(BuildContext context) { return MaterialApp(
...

class MyHomePage extends StatefulWidget {
  final List<CameraDescription> cameras;
  final String title;

  const MyHomePage({Key key, this.title, @required this.cameras})
      : super(key: key);

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

これでOK!やっと真っ赤な画面が消えました。

 

やっとここまで来ました。それでは、写真を撮ってみましょう。

シミュレータでカメラを起動してみると。

遭遇したエラー NoSuchMethodError: The getter 'name' was called on null

NoSuchMethodError: The getter 'name' was called on null

なんでやねん。

ただ、これはもしかしたらシミュレータのせいかもと思って実機で確認してみたら、うまくいってました。

camera.first(背面カメラ)からの景色がちゃんと写っています!

カメラなどデバイス(ハード)の機能をFlutterから呼び出そうとすると、こんな感じでシミュレータがついてきてないことも多いので、落ち着いて実機で試してみることをお勧めします。

 

早速、右下のカメラボタンで撮影してみると、、、

 

遭遇したエラー  The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888

====================================================================================================
The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888

======== Exception caught by image resource service ================================================
The following FileSystemException was thrown resolving an image codec:
Cannot open file, path = '/var/mobile/Containers/Data/Application/xxx/Documents/2021-05-22 07:59:19.090280.png' (OS Error: No such file or directory, errno = 2)

なんかまた見たことないエラーが出ました。

これは、撮影した写真ファイルを保存する場所として指定したパスうまくいってない感じですね。

<解決方法>

すみません、いじりすぎてどこをどう直したのかよくわからなくなりました。

とりあえず「take_picture.dart」というファイルを新規に作成し、公式のカメラ撮影のお手本コードをまるっとコピペしたら上手くいきました。

↓実機(iPhoneXR)のスクショ

カメラボタンを押すと、

OK!撮れてます!

ここに、保存ボタンがないですね。つけていきましょう。

 

撮影した写真を保存する

撮影した写真を表示する画面に、保存ボタンを追加します。

lib/take_picture.dart

import 'dart:io';

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

class TakePictureScreen extends StatefulWidget {
...
}

// カメラのリストと、イメージを保存するディレクトリーを取り入れるスクリーン。
class TakePictureScreenState extends State {
...
}

// A widget that displays the picture taken by the user.
class DisplayPictureScreen extends StatelessWidget {
  final String tmpImagePath;

  const DisplayPictureScreen({Key key, @required this.tmpImagePath})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Display the Picture')),
        // The image is stored as a file on the device. Use the `Image.file`
        // constructor with the given path to display the image.
        body: Image.file(File(tmpImagePath)),
        floatingActionButton: Center(
            child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
              Container(
                  // margin: EdgeInsets.only(left: 30.0, bottom: 10.0),
                  child: FloatingActionButton.extended(
                label: Text('これでOK'),
                tooltip: 'OK',
                onPressed: () async{
                  // pathパッケージを使ってイメージが保存されるパスを構成する
                  final imagePath = join(
                      (await getApplicationDocumentsDirectory()).path,
                  '${DateTime.now()}.png',
                  );
                  // ローカルに保存
                  await File(imagePath).writeAsBytes(await File(tmpImagePath).readAsBytes());
                  print('imagePath============= $imagePath');

                  //ふたつ前の画面に戻る
                  int count = 0;
                  Navigator.popUntil(context, (_) => count++ >= 2);

                },
              ))
            ])));
  }
}

完成系のソースコード

変更箇所が多く、文章では伝わりにくいので、最終的なソースをみたい人はGitHubに公開しているソースをご確認ください。

a-sueki/gomi_map撮影したごみをGoogle Map上に表示するアプリ(ゴミ拾いする人特化型). Contribute to a-sueki/gomi_map development by creating an account on GitHub.
a-sueki/gomi_map github.com
a-sueki/gomi_map

 

 

それでは!

 

ここまで読んでいただき、ありがとうございました!

参考になったよ、という方はTwitter(@suekiaoi)やInstagram(@aoi.sueki)などでフォローしていただけると励みになります。

不明点・ご質問などありましたらわたしのTwitter DM(@suekiaoi)にお気軽にどうぞ!

それでは!

末岐 碧衣
  • 末岐 碧衣
  • フリーランス のシステムエンジニア。独立後、一度も営業せずに月収 96 万円を達成。1986年大阪生まれ。早稲田大学理工学部卒。システムエンジニア歴 12年。
    2009年、ITコンサルティング企業に入社。3年目でコミュ障が爆発し人間関係が崩壊。うつにより休職するも、復帰後はコミュ障の自覚を持ち、「チームプレイ」を徹底的に避け、会社組織内においても「一人でできる仕事」に専念。社内外から評価を得た。
    無理に「チームプレイ」するよりも「一人でできる仕事」に専念した方が自分も周囲も幸せにできることを確信し、2015年フリーランスとして独立。