5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Android】Jetpack Composeで、イメージピッカーを作ってみる【rememberLauncherForActivityResult()編】

Posted at

イメージピッカーをJetpack Composeで作ってみたい

なにかしらの登録画面で、ギャラリーから画像を選択して、その選択した画像を登録できるようにしたいと思ったことはありませんか?しかも、Jetpack Composeを使って。

僕はこの問題に、今まさに直面しています。

そこでこの記事では、未来の自分への忘備録として、Jetpack Composeでイメージピッカーを作成する手順を解説しようと思います。

rememberLauncherForActivityResult()とは

実際に、イメージピッカーの実装に入る前に、あるメソッドについて説明しておかないといけません。
そのメソッドとは、rememberLauncherForActivityResult()のことです。

コンポーザブル内で、ファイルを選択したり保存したりする機能を実装したことがある方なら見覚えがあるかもしれません。

公式のドキュメントには次のように書かれています。

rememberLauncherForActivityResult() API を使用すると、コンポーザブル内のアクティビティの結果を取得できます。
https://developer.android.com/jetpack/compose/libraries?hl=ja

この、コンポーザブル内のアクティビティの結果とはなんなのでしょうか?

具体的に言うと、コンポーザブル内で発行されたインテントにより、起動されたアクティビティが完了した段階で、登録されたコールバックに渡される値という意味になります。

rememberLauncherActivityForResult()を呼び出す際には、2つの引数が必要になります。1つがActivityResultContract、そしてもう1つが結果を受け取るコールバック関数です。

contract引数で渡すActivityResultContractとは

rememberLauncherForActivityResult()contract引数はActivityResultContractを表しています。

このActivityResultContractとは一体何を表しているのでしょうか?

ActivityResultContractはコントラクトと呼ばれるものです。

コールバックによって受け取る結果を生成するためには、アクティビティを起動する必要があります。そのアクティビティを起動するためにはインテントを発行する必要があります。

そしてこの、結果を生成する一連のプロセスを開始する際に呼び出されるメソッド(launch())に渡す引数のクラスと、アクティビティの起動後に呼び出されるコールバックに対して渡される値のクラスを定義するためのものがコントラクトとなります。

このコントラクトには、あらかじめデフォルトのコントラクトが用意されています。
例えば、写真を撮影するためにはカメラのアクティビティを起動する必要があります。そのためのインテントを発行するためのコントラクトがデフォルトで用意されています。あとは権限のリクエストを行うためのコントラクトなども用意されています。

デフォルトのコントラクトには、次のようにしてアクセスします。

ActivityResultContracts.GetMultipleContents()

onResult引数で渡すコールバックとは

コールバックでは、コントラクトによって定義された結果の型で表現される値が引数として渡されます。

例えば、今回の場合は画像のuriを取得する必要があります。
そして、コントラクトとして渡される値は、ActivityResultContracts.GetContent()になります。

そのため、アクティビティの完了の結果として渡される値はUri型の値となるのです。

rememberLauncherForActivityResult(contract =
    ActivityResultContracts.GetContent()
) { uri: Uri? ->
    // TODO: 受け取ったuriを元に処理を行う
}

ManagedActivityResultLauncherとは

アクティビティの起動は、rememberLauncherForActivityResult()が行うわけではありません。
このメソッドの呼び出しによって返されるManagedActivityResultLauncherインスタンスを使って、アクティビティの起動を任意のタイミングで行う必要があります。

この、インテントを発行してアクティビティを起動する処理を行うために呼び出されるのが、このインスタンスのlaunch()メソッドです。

rememberLauncherForActivityResult()を使う際のコールバックの登録から呼び出しまでの流れは、実際には次のようになります。

  • rememberLauncherForActivityResult()にコントラクトとコールバックを渡して呼びして、ManageActivityResultLauncherインスタンスを取得する。
  • ManageActivityResultLauncherインスタンスのlaunch()を呼び出す。
  • アクティビティが完了してアプリに復帰すると、生成された結果と共に事前に登録されているコールバックが呼び出される。

作成したい機能

ImagePickerコンポーザブルで実装したい機能をリストにしてまとめます。

  • 画像を端末内のギャラリーから選択できる
  • 選択した画像をプレビューとして表示する
  • 選択した画像のuriをコールバックを通して取得できる

完成したImagePickerコンポーザブルの絵

上記でまとめた機能を実装すると、次のようになりました。

まず、何も表示されていない真っ黒なプレビュー部分と、画像を選択するアクティビティを起動するボタンを配置します。

1.png

ボタンをクリックすると、新しいアクティビティが開きます。
ここでは、端末内にダウンロードされた画像の一覧が表示されています。

2.png

画像をタップすると、元のアプリに復帰して、真っ黒だったプレビュー部分に選択した画像が表示されます。

3.png

ImagePickerコンポーザブルのコード

上記の内容を表現したコードが次のようになります。

@RequiresApi(Build.VERSION_CODES.P)
@Composable
fun ImagePicker(
    context: Context,
    onResult: (uri: Uri) -> Unit
) {
    var imageUri: Uri? by remember {
        mutableStateOf(null)
    }

    var bitmap: Bitmap? by remember {
        mutableStateOf(null)
    }

    val launcher =
        rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri ->
            if (uri == null) return@rememberLauncherForActivityResult
            imageUri = uri
            onResult(uri)
        }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(256.dp)
                .background(Color(0xFF000000))
                .border(
                    width = 1.dp,
                    color = MaterialTheme.colorScheme.primary,
                ),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            imageUri?.let {
                val source = ImageDecoder
                    .createSource(
                        context.contentResolver,
                        it,
                    )
                bitmap = ImageDecoder.decodeBitmap(source)

                bitmap?.let { bm ->
                    Image(
                        bitmap = bm.asImageBitmap(),
                        contentDescription = null,
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            }
            Spacer(modifier = Modifier.height(8.dp))

            if (imageUri == null) {
                Text(
                    color = Color(0xFFFFFFFF),
                    text = stringResource(id = R.string.image_picker_text)
                )
            }
        }
        Spacer(modifier = Modifier.height(8.dp))
        OutlinedButton(
            modifier = Modifier
                .testTag("AddButton")
                .fillMaxWidth(),
            onClick = {
                launcher.launch("image/*")
            },
        ) {
            Text(
                text = stringResource(id = R.string.select_image)
            )
        }
    }
}

rememberLauncherForActivityResult()で登録したコールバックの中で、選択した画像のuriを状態として保存しています。

Bitmapを使って画像を表示する方法の解説

紹介したコードでは、ImageDecoder.createSource()を使って、取得したUriImageDecoder.Source型に変換しています。

val source = ImageDecoder
    .createSource(
        context.contentResolver,
        it,
    )

それをImageDecoder.decodeBitmap()に渡してBitmapを生成、asImageBitmap()を呼び出して画像を表示させています。

bitmap = ImageDecoder.decodeBitmap(source)

Image(
    bitmap = bm.asImageBitmap(),
    contentDescription = null,
    modifier = Modifier.fillMaxWidth()
)

Bitmapを使わないで簡単に画像を表示する方法

実は、わざわざBitmapに変換せずとも簡単に画像を表示する方法があります。
それはAsyncImageコンポーザブルを使う方法です。

AsyncImageはCoilライブラリに存在するコンポーザブルです。使用する場合は、build.gradleで次のインポートを行ってください。

implementation("io.coil-kt:coil-compose:2.2.2")

AsyncImageを使えば、imageUriを渡すだけで、簡単に画像を表示させることができます。

AsyncImage(model = imageUri, contentDescription = null)

なぜ、AsyncImageを最初から使っていないかと言うと、このImagePickerコンポーザブルを作り始めていた当初はAsyncImageの採用を考えていました。

しかし、画像を表示させることができませんでした。試行錯誤しながらたどり着いたのが、このBitmapを使う方法だったのです。

しかし、この記事を書いている最中にもう一度試してみたところ、AsyncImageでも画像を表示させることができるようになっていました。(あの頃の努力は一体・・・)

ならばAsyncImageで画像を表示するように書き換えれば良いと思うかもしれません。が、もしかしたら僕の他の設定が原因でたまたまAsyncImageでも表示できるようになっているという疑惑を払拭することができなかったため、汎用的な方法であるBitmapを採用させていただきました。

もしAsyncImageが使えるのなら、ぜひAsyncImageを使用することをおススメします。とても簡単です。

もしも権限エラーが出た場合・・・

おそらく、今回の使用用途で発生することはないと思うのですが、外部ストレージやインターネット上のデータを取得する際には関連する権限のエラーが発生する可能性があります。

そこで、そのエラーを回避するために、AndroidManifest.xmlに次の<uses-permission />を追加してください。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

データストレージに対する使用の用途や権限に関する詳細については、こちらのドキュメントを参照してください。

ネットワークに接続する際の権限については、こちらのドキュメントを参照してください。

まとめ

アプリを開発していて、ユーザーが写真を投稿できる機能を実装したい場合に、必ずイメージピッカーの問題に直面すると思います。その際にサードパーティ製のライブラリを使おうか迷うと思いますが、rememberLauncherForActivityResult()を使って簡単にカスタマイズ可能なイメージピッカーを実装することができます。

ぜひ、みなさんも自分がデザインしたイメージピッカーを実装してみてください!

参考にした記事

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?