新人エンジニアのつぶやき

日々の学びをアウトプットするためのブログです

【Flutter】 Andoroidでimage_picker を使う際はAndroidManifestを修正しよう

この記事について

いいコードはいい心から

最近、ネイティブアプリの勉強してみたいなと思い、flutterを触っています。Webアプリとは別物だなと感じ、色々な壁にぶち当たっては怪我ばかりしているのですが、今回はその中の1つです。image_pickerというライブラリについてです。こちら、OSのバージョンによって使い方が少し異なるので注意です。今回はAndroidで問題が起こったので、まとめました!Flutter、、、仲良くしたい、、、

image_pickerとは

動作確認動画

実装について

実装手順は以下の通りです。

1:プラグインをpubspec.yamlに追加する (実装当初は0.8.6を使用しているが現在は1.0.4なので、バージョン値は変更する必要がある)

dependencies:
  flutter:
    sdk: flutter

  # 画像ライブラリから画像を選択、もしくはカメラで新しい写真を撮るため
  image_picker: ^0.8.6

2:実装する

import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';

class ImageAddPage extends StatefulWidget {
  const ImageAddPage({Key? key}) : super(key: key);
  @override
  State<ImageAddPage> createState() => _ImageAddPageState();
}

class _ImageAddPageState extends State<ImageAddPage> {
  XFile? _image;
  final imagePicker = ImagePicker();

  // カメラで撮影した画像を取得するメソッド
  Future getImageFromCamera() async {
    final pickedFile = await imagePicker.pickImage(source: ImageSource.camera);
    setState(() {
      if (pickedFile != null) {
        _image = XFile(pickedFile.path);
      }
    });
  }
  
  // ギャラリーから画像を取得するメソッド
  Future getImageFromGalley() async {
    final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery);
    setState(() {
      if (pickedFile != null) {
        _image = XFile(pickedFile.path);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body : Padding(
        padding: const EdgeInsets.only(right: 16.0, left: 16.0),
        child: Column(
        children: [
          Center(
            child: _image == null 
            ? Container(
              width: 350,
              height: 250,
              decoration: BoxDecoration(
                color: Colors.blueGrey[100],
                borderRadius: BorderRadius.circular(10),
              ),
              child: const Icon(Icons.camera_alt_outlined,  size: 200,),
            )
            : Image.file(
                File(_image!.path),
                width: 350,
                height: 250
              )
          ),
        ],
      ),  
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
        children: [
          // カメラから取得するボタン
          FloatingActionButton(
            heroTag: 'camera',
            onPressed: getImageFromCamera,
            child: const Icon(Icons.photo_camera)

          ),
          // ギャラリーから取得するボタン
          FloatingActionButton(
            heroTag: 'gallery',
            onPressed: getImageFromGalley,
            child: const Icon(Icons.photo_album)
          )
        ]
      )
    );
  }
}

※先ほどGifで写っている成果物と一部実装が異なります。

これでビルドすることで使用することができます。

なんか動かない。。。

今回、Android 13(API レベル 33)で動作確認を行い、問題なく使えることを確認しました。そのため、安心していたのですが、端末によっては動かないものがあることが後々わかりました。調べてみると、Android 12(API レベル 31、32)以下で、image_pickerが実装されている機能を動かした場合に画面が固まってしまうという事象が発生するのです。なぜだ。。。

調査

  • ログ見るとライブラリの中でのエラーぽい→ NullPointerExceptionで落ちている

ログの詳細はこちら

05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android: Failed to handle method call
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android: java.lang.NullPointerException
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Objects.requireNonNull(Objects.java:220)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Arrays$ArrayList.<init>(Arrays.java:4248)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Arrays.asList(Arrays.java:4235)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.f.a(Unknown Source:16)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.f.b(Unknown Source:15)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e$a.b(Unknown Source:2)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e.E(Unknown Source:6)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e.J(Unknown Source:10)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.ImagePickerPlugin.f(Unknown Source:229)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at c0.j$a.a(Unknown Source:17)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.k(Unknown Source:18)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.l(Unknown Source:40)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.h(Unknown Source:0)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.b.run(Unknown Source:12)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Handler.handleCallback(Handler.java:938)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Handler.dispatchMessage(Handler.java:99)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Looper.loopOnce(Looper.java:226)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Looper.loop(Looper.java:313)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.app.ActivityThread.main(ActivityThread.java:8582)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.lang.reflect.Method.invoke(Native Method)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:563)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
05-02 18:21:13.619 22705 22730 I flutter : PlatformException(error, null, null, java.lang.NullPointerException
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Objects.requireNonNull(Objects.java:220)
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Arrays$ArrayList.<init>(Arrays.java:4248)
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Arrays.asList(Arrays.java:4235)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.f.a(Unknown Source:16)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.f.b(Unknown Source:15)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e$a.b(Unknown Source:2)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e.E(Unknown Source:6)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e.J(Unknown Source:10)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.ImagePickerPlugin.f(Unknown Source:229)
05-02 18:21:13.619 22705 22730 I flutter :  at c0.j$a.a(Unknown Source:17)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.k(Unknown Source:18)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.l(Unknown Source:40)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.h(Unknown Source:0)
05-02 18:21:13.619 22705 22730 I flutter :  at p.b.run(Unknown Source:12)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Handler.handleCallback(Handler.java:938)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Handler.dispatchMessage(Handler.java:99)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Looper.loopOnce(Looper.java:226)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Looper.loop(Looper.java:313)
05-02 18:21:13.619 22705 22730 I flutter :  at android.app.ActivityThread.main(ActivityThread.java:8582)
05-02 18:21:13.619 22705 22730 I flutter :  at java.lang.refl

  • Android 12のカメラが起動しないことに関して調査を進めたところ
    • キャプチャ取得にIntentクラスを使用している
    • カメラが起動しない状態でライブラリを起動しようとしても起動しないのは状態が切り替わらないから
  • 権限を付与するかの確認画面が表示されなかった..

ネイティブアプリの実装者ならば、気付いている方もいらっしゃると思いますが、AndroidManifest.xmlを修正していないためにこの事象は発生していました。

AndroidManifest.xmlというのはAndroidアプリに関する情報をAndroidのOS(システム)に伝える役目を担っており、コード実行の際に必要になる重要な情報を記述する必要があります。

ここで追記が必要なのはpermission(認可、許諾)に関する設定です。今回カメラやギャラリーを使います。この時に、ユーザーに許可をもらわないといけません。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.example">
    <uses-permission android:name="android.permission.CAMERA" /> 
   <application
        android:label="HOGE"
        android:name="${applicationName}"
        android:icon="@mipmap/launcher_icon">
        <activity
            android:name="io.example"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

今回、<uses-permission android:name="android.permission.CAMERA" />を足すことで、ユーザーに対して、許可をもらうためのダイアログが表示されるようになり、許可した場合にはカメラやギャラリーにアクセスすることができるようになりました。

ちなみに、iOSの場合はInfo.plistというファイルに色々追記する必要があります。以下は例です。

<key>NSPhotoLibraryUsageDescription</key>
<string>フォトライブラリを利用する理由を記載</string>
<key>NSCameraUsageDescription</key>
<string>カメラを利用する理由を記載</string>
<key>NSMicrophoneUsageDescription</key>
<string>マイクを利用する理由を記載</string>

皆様もきちんと記載していきましょうね。。。私もちゃんと書きます。

  • AndroidManifest.xmlの修正により事象の解決には至ったのですが、ドキュメントには以下のような記載がありました。
// 原本
Android
Starting with version 0.8.1 the Android implementation support to pick (multiple) images on Android 4.3 or higher.

No configuration required - the plugin should work out of the box. It is however highly recommended to prepare for Android killing the application when low on memory. How to prepare for this is discussed in the [Handling MainActivity destruction on Android](https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker#handling-mainactivity-destruction-on-android) section.

It is no longer required to add android:requestLegacyExternalStorage="true" as an attribute to the <application> tag in AndroidManifest.xml, as image_picker has been updated to make use of scoped storage.


// 日本語
バージョン0.8.1より、Android 4.3以降のAndroid実装で、画像のピック(複数枚)をサポートしました。

設定は不要で、プラグインはすぐに動作するはずです。ただし、メモリ不足の際にAndroidがアプリケーションを終了させることを想定しておくことを強くお勧めします。その方法については、「AndroidにおけるMainActivityの破壊の処理」のセクションで説明しています。

image_pickerがscoped storageを使用するように更新されたため、AndroidManifest.xmlの<application>タグの属性としてandroid:requestLegacyExternalStorage="true "を追加する必要がなくなりました。
  • ドキュメントにはandroid:requestLegacyExternalStorage="true "を追加する必要がなくなりました。と記載もあり、特に設定する必要はないように感じました。しかし、このようなことがあるので、ドキュメントを見つつ動かしていく必要があることを改めて認識しました。

最後に

ネイティブアプリの実装とWebアプリの実装は日本語習得と英語習得くらい違うと思っているので、バイリンガルになれるようにインプット、アウトプットを続けていこうと思います。