イメージピッカーを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コンポーザブルの絵
上記でまとめた機能を実装すると、次のようになりました。
まず、何も表示されていない真っ黒なプレビュー部分と、画像を選択するアクティビティを起動するボタンを配置します。
ボタンをクリックすると、新しいアクティビティが開きます。
ここでは、端末内にダウンロードされた画像の一覧が表示されています。
画像をタップすると、元のアプリに復帰して、真っ黒だったプレビュー部分に選択した画像が表示されます。
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()
を使って、取得したUri
をImageDecoder.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()
を使って簡単にカスタマイズ可能なイメージピッカーを実装することができます。
ぜひ、みなさんも自分がデザインしたイメージピッカーを実装してみてください!
参考にした記事