Flutter: cameraパッケージで初回の許可ダイアログ表示後にエラー
結論:permission_handlerを使い、initializeする前段階で許可ダイアログを出す
2022/12/10 Flutter エラー・バグ日記
「camera」パッケージを使ったアプリをAndroidでビルドすると、初回起動時に表示されるカメラアクセス許可のダイアログ(下図)が表示された後、エラーになった。
エラーの内容は以下。
CameraException(Uninitialized CameraController, getMaxZoomLevel() was called on an uninitialized CameraController.) ・・(略)・・ E/AndroidRuntime( 6867): FATAL EXCEPTION: CameraBackground E/AndroidRuntime( 6867): Process: com.XXXX.YYYY, PID: 6867 E/AndroidRuntime( 6867): java.lang.NullPointerException: Attempt to invoke virtual method 'void android.hardware.camera2.CameraCaptureSession.close()' on a null object reference E/AndroidRuntime( 6867): at io.flutter.plugins.camera.Camera.closeCaptureSession(Camera.java:1164) ・・(略)・・
該当箇所のコードは以下のとおり。エラー文を見る限り、コントローラーを「initialize」メソッドで初期化後、カメラのズームレベルを取得する所でエラーになっているので、初期化処理がうまく行っていない模様。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの) // の名前の末尾には、大文字のオー「O」をつけています // ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、 // 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため // cameraControllerOの初期化メソッド ※cameraControllerOはCameraControllerのインスタンス await cameraControllerO!.initialize(); // ズームレベルの取得 ↓↓たぶんココでエラーになっている await cameraControllerO! .getMaxZoomLevel() .then((double valueO) => maxAvailableZoomO = valueO); await cameraControllerO! .getMinZoomLevel() .then((double valueO) => minAvailableZoomO = valueO);
エラーは初回起動時のみで、アプリを再起動すると、Cameraのプレビューが表示され、問題なく動作する。
しかし、必ず1回はアプリが落ちる、という状況は避けたい。。
実行環境
各種バージョンは、Flutter 3.3.4、Dart 2.18.2、camera: ^0.10.0+4。
エラーは、Android10(SDK30)、Android12(SDK31)、Android13(SDK33)いずれのエミュレーターでも発生する。
実機でも発生するので、エミュレーターの問題ではなさそう。
「許可ダイアログ表示後にエラーが出る」という内容でググると、以下の報告が見つかったが、Android13のみで発生する内容のため、自分の状況とは異なる。
参考にしたコード
パッケージのサンプルコードも参考にしつつ、メインは、下記Flutter公式サイトのサンプルコードを参考にしていた。
このサンプルコードのままであれば問題ないが、これをベースに改変を加えた結果、エラーが生じる状況になってしまった。
コードが肥大化しているため、全体像を掲載することは難しいが、基本的な骨組みはサンプルコードと同じはずで、
- 「availableCameras」メソッドでカメラ情報を取得
- 「CameraController」のインスタンス(コントローラー)を作成
- 「initialize」メソッドでコントローラーを初期化
- 「CameraPreview」クラスでプレビュー画面を表示
の順番で実装している。
パッケージのReadmeにある「build.gradle」の設定や、ライフサイクルの実装も対応している。
Camera.javaの修正が必要”だった”らしい
さらに調べると、2019年の記事だが、全く同じエラーについて解説があった。
Cameraパッケージの「Camera.java」ファイルに、
if (pictureImageReader != null)
という一文を追記して、エラーを回避するとのこと。
しかし、「Camera.java」の中身を確認すると、ほぼ同じ趣旨と思われる
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
という一文が既に入っており、解消済の様子だった(上記2019年の記事で使用されている「camera」パッケージのバージョンは「0.5.5+1」なので、現在よりもかなり古い)。
ライフサイクル関連が怪しい
コード内各所にprint文を入れてチェックしたところ、許可ダイアログが出ると、アプリのライフサイクルが「非アクティブ」状態になり、「didChangeAppLifecycleState」メソッドが発動されているとが分かった。
前述のとおり、ライフサイクル対応は、Readmeのサンプルコードに従って実装したが、うまく機能していない模様。
「Future.delayed」で待ち時間を作ると解消する
そもそも許可ダイアログはどこで発動するのか?をprint文を入れて確認したところ、コントローラーを初期化する「initialize」メソッドの所だと分った。
そこで、試しに「initialize」メソッドの後に、以下のように3秒ほど待ち時間を入れてみた。
await cameraControllerO!.initialize(); Future.delayed(Duration(seconds: 3));
すると、エラーが解消された。
ということは、、許可ダイアログが表示されると、アプリが非アクティブ状態になるので、
許可取得→初期化処理→ズームデータの取得
を順番どおりに処理できないのかもしれない(この点、ライフサイクルへの理解が乏しく、不確かではあるが、、)。
permission_handlerで事前に許可取得することにした
「Future.delayed」は、無駄な待ち時間が発生し、かつエラーを確実に回避できる保証も無いため、できれば採用したくない。。
そこで、許可ダイアログと「initialize」メソッドを同時に発動させないように修正することにした。
具体的には、アプリが起動した直後に、先に許可ダイアログを出して、カメラへのアクセス許可を取得しておき、それから離れたタイミングで、「initialize」メソッドを実行するようにした。
具体的には、「permission_handler」パッケージを導入し、以下のように、カメラ情報を取得する前の段階で、許可ダイアログを表示させるようにした。
// permission_handlerによるカメラアクセス許可ダイアログの表示 await Permission.camera.request(); // デバイスで使用可能なカメラのリストを取得 final camerasO = await availableCameras(); // コントローラーの設定や初期化はこの後で実行
これにより、「initialize」メソッドの所では許可ダイアログが表示されないようになり、無事、エラーも解消された。
根本原因にたどり着けず、今ひとつスッキリしない感はあるが、、調べているとキリが無さそうなので、いったん区切りを付けることにする。
\一般的なエラー対処法をまとめた記事はこちら/
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/